@svta/cml-cmcd 2.6.1 → 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,16 +554,21 @@ 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
 
@@ -528,7 +578,7 @@ function isValid(value) {
528
578
  * Resolve the text of a token value.
529
579
  *
530
580
  * A token may be a plain string, a registry `Symbol`, an `SfToken`, or an
531
- * `SfItem` that wraps one of those.
581
+ * `SfItem` that wraps such a value.
532
582
  *
533
583
  * @param value - The value to resolve.
534
584
  *
@@ -545,11 +595,11 @@ function toTokenString(value) {
545
595
  //#endregion
546
596
  //#region src/prepareCmcdData.ts
547
597
  const filterMap = {
548
- [CMCD_EVENT_MODE]: isCmcdEventKey,
549
- [CMCD_REQUEST_MODE]: isCmcdRequestKey
598
+ event: isCmcdEventKey,
599
+ request: isCmcdRequestKey
550
600
  };
551
601
  /**
552
- * Unwrap an inner list or SfItem value to a plain scalar.
602
+ * Unwrap an inner list or SfItem value to a scalar.
553
603
  */
554
604
  function unwrapValue(value, ot) {
555
605
  if (Array.isArray(value)) {
@@ -563,10 +613,10 @@ function unwrapValue(value, ot) {
563
613
  return value;
564
614
  }
565
615
  /**
566
- * Down-convert V2 CMCD data to V1 format.
616
+ * Down-convert version 2 CMCD data to version 1.
567
617
  *
568
- * - Extracts `nrr` from `nor` SfItem `r` parameter.
569
- * - Unwraps inner-list values to plain scalars.
618
+ * - Extracts `nrr` from the `nor` SfItem's `r` parameter.
619
+ * - Unwraps inner-list values to scalars.
570
620
  */
571
621
  function downConvertToV1(obj) {
572
622
  const result = {};
@@ -586,6 +636,10 @@ function downConvertToV1(obj) {
586
636
  }
587
637
  return result;
588
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
+ }
589
643
  /**
590
644
  * Convert a generic object to CMCD data.
591
645
  *
@@ -627,10 +681,23 @@ function prepareCmcdData(obj, options = {}) {
627
681
  baseUrl: options.baseUrl
628
682
  };
629
683
  keys.sort();
684
+ let objectType;
685
+ let objectTypeResolved = false;
630
686
  for (const key of keys) {
631
687
  let value = data[key];
632
- const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
633
- 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);
634
701
  if (key === "v") if (version === 1) continue;
635
702
  else value = version;
636
703
  if (key === "pr" && value === 1 && !(isEventMode && eventType === CMCD_EVENT_PLAYBACK_RATE)) continue;
@@ -703,9 +770,9 @@ const CMCD_HEADER_FIELDS = [
703
770
  /**
704
771
  * The map of CMCD keys to their appropriate header shard.
705
772
  *
706
- * Note: Event-only keys (e, ts, cen, h) and response-received keys
707
- * (rc, ttfb, ttlb, url, etc.) are intentionally absent. They are
708
- * 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.
709
776
  *
710
777
  * @public
711
778
  */
@@ -760,9 +827,9 @@ function createHeaderMap(headerMap) {
760
827
  }, {});
761
828
  }
762
829
  /**
763
- * Group a CMCD data object into header shards
830
+ * Group a CMCD object into header shards
764
831
  *
765
- * @param cmcd - The CMCD data object to convert.
832
+ * @param cmcd - The CMCD object to convert.
766
833
  * @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
767
834
  *
768
835
  * @returns The CMCD header shards.
@@ -808,10 +875,10 @@ function toPreparedCmcdHeaders(data, customHeaderMap) {
808
875
  //#endregion
809
876
  //#region src/toCmcdHeaders.ts
810
877
  /**
811
- * Convert a CMCD data object to request headers
878
+ * Convert a CMCD object to request headers
812
879
  *
813
- * @param cmcd - The CMCD data object to convert.
814
- * @param options - Options for encoding the CMCD object.
880
+ * @param cmcd - The CMCD object to convert.
881
+ * @param options - Options for encoding.
815
882
  *
816
883
  * @returns The CMCD header shards.
817
884
  *
@@ -830,13 +897,13 @@ function toCmcdHeaders(cmcd, options = {}) {
830
897
  //#endregion
831
898
  //#region src/appendCmcdHeaders.ts
832
899
  /**
833
- * Append CMCD query args to a header object.
900
+ * Append CMCD header shards to a header object.
834
901
  *
835
902
  * @param headers - The headers to append to.
836
903
  * @param cmcd - The CMCD object to append.
837
- * @param options - Encode options.
904
+ * @param options - Options for encoding.
838
905
  *
839
- * @returns The headers with the CMCD header shards appended.
906
+ * @returns The headers with CMCD header shards appended.
840
907
  *
841
908
  * @public
842
909
  *
@@ -897,10 +964,10 @@ function encodeCmcd(cmcd, options = {}) {
897
964
  //#endregion
898
965
  //#region src/toCmcdUrl.ts
899
966
  /**
900
- * Convert a CMCD data object to a URL encoded string.
967
+ * Convert a CMCD object to a URL encoded string.
901
968
  *
902
969
  * @param cmcd - The CMCD object to convert.
903
- * @param options - Options for encoding the CMCD object.
970
+ * @param options - Options for encoding.
904
971
  *
905
972
  * @returns The URL encoded CMCD data.
906
973
  *
@@ -917,10 +984,10 @@ function toCmcdUrl(cmcd, options = {}) {
917
984
  //#endregion
918
985
  //#region src/toCmcdQuery.ts
919
986
  /**
920
- * Convert a CMCD data object to a query arg.
987
+ * Convert a CMCD object to a query arg.
921
988
  *
922
989
  * @param cmcd - The CMCD object to convert.
923
- * @param options - Options for encoding the CMCD object.
990
+ * @param options - Options for encoding.
924
991
  *
925
992
  * @returns The CMCD query arg.
926
993
  *
@@ -944,7 +1011,7 @@ const REGEX = /CMCD=[^&#]+/;
944
1011
  *
945
1012
  * @param url - The URL to append to.
946
1013
  * @param cmcd - The CMCD object to append.
947
- * @param options - Options for encoding the CMCD object.
1014
+ * @param options - Options for encoding.
948
1015
  *
949
1016
  * @returns The URL with the CMCD query args appended.
950
1017
  *
@@ -965,7 +1032,7 @@ function appendCmcdQuery(url, cmcd, options) {
965
1032
  //#endregion
966
1033
  //#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
967
1034
  /**
968
- * The default time interval in seconds when using using event mode
1035
+ * The default time interval in seconds when using event mode
969
1036
  *
970
1037
  * @public
971
1038
  */
@@ -997,25 +1064,26 @@ const CMCD_MIME_TYPE = "application/cmcd";
997
1064
  //#endregion
998
1065
  //#region src/CMCD_REQUEST_PROVENANCE.ts
999
1066
  /**
1000
- * The `customData` key under which `CmcdReporter.createRequestReport()`
1001
- * stamps session provenance on every request it returns.
1067
+ * The `customData` property where `CmcdReporter.createRequestReport()`
1068
+ * writes session provenance on every request it returns.
1002
1069
  *
1003
- * The value is a frozen `CmcdRequestProvenance` record: `sid` names the
1004
- * session that issued the request, `cid` is the content id in effect when
1005
- * the request was issued, and `data` carries the request's per-call data
1006
- * as an encoded CMCD string. `CmcdReporter.recordResponseReceived()` reads
1007
- * the record to attribute a late response to its session and to rebuild
1008
- * the request-time report keys.
1009
- * Spread and `Object.assign` carry the record through ordinary request
1010
- * clones, but `JSON.stringify` and structured clone drop symbol-keyed
1011
- * properties: read the value before such a boundary and restore it
1012
- * afterward. The record itself survives JSON, and attribution is by the
1013
- * `sid` value rather than object identity, so a revived plain-object copy
1014
- * attributes exactly. A record naming a `sid` the reporter does not retain
1015
- * 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.
1016
1083
  *
1017
- * Backed by the symbol registry so duplicated copies of this library in
1018
- * 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.
1019
1087
  *
1020
1088
  * @public
1021
1089
  */
@@ -1056,7 +1124,7 @@ const CmcdObjectType = {
1056
1124
  //#endregion
1057
1125
  //#region src/CmcdPlayerState.ts
1058
1126
  /**
1059
- * CMCD v2 player states for the 'sta' key.
1127
+ * CMCD version 2 player states for the 'sta' key.
1060
1128
  *
1061
1129
  *
1062
1130
  * @enum
@@ -1081,29 +1149,29 @@ const CmcdPlayerState = {
1081
1149
  //#endregion
1082
1150
  //#region src/CmcdRecordedReportMode.ts
1083
1151
  /**
1084
- * CMCD query reporting mode — CMCD data carried as a `CMCD=` URL
1152
+ * CMCD query reporting mode: CMCD data transmitted as a `CMCD=` URL
1085
1153
  * query parameter.
1086
1154
  *
1087
1155
  * @public
1088
1156
  */
1089
1157
  const CMCD_RECORDED_REPORT_MODE_QUERY = "query";
1090
1158
  /**
1091
- * CMCD header reporting mode — CMCD data carried as `Cmcd-*` HTTP
1159
+ * CMCD header reporting mode: CMCD data transmitted as `CMCD-*` HTTP
1092
1160
  * headers.
1093
1161
  *
1094
1162
  * @public
1095
1163
  */
1096
1164
  const CMCD_RECORDED_REPORT_MODE_HEADER = "header";
1097
1165
  /**
1098
- * 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
1099
1167
  * event-target URL.
1100
1168
  *
1101
1169
  * @public
1102
1170
  */
1103
1171
  const CMCD_RECORDED_REPORT_MODE_EVENT = "event";
1104
1172
  /**
1105
- * Reporting mode under which a captured report was observed by
1106
- * `CmcdReportRecorder`.
1173
+ * Reporting mode in which `CmcdReportRecorder` observed a captured
1174
+ * report.
1107
1175
  *
1108
1176
  * @enum
1109
1177
  *
@@ -1137,7 +1205,7 @@ const CMCD_RECORDED_REQUEST_TYPE_SEGMENT = "segment";
1137
1205
  */
1138
1206
  const CMCD_RECORDED_REQUEST_TYPE_EVENT = "event";
1139
1207
  /**
1140
- * Fallback classification for requests the recorder did not recognize
1208
+ * Default classification for requests the recorder did not recognize
1141
1209
  * as manifest, segment, or event.
1142
1210
  *
1143
1211
  * @public
@@ -1179,17 +1247,17 @@ async function toHttpRequest(request) {
1179
1247
  };
1180
1248
  }
1181
1249
  /**
1182
- * Create a transport adapter that patches `globalThis.fetch` to capture
1183
- * CMCD-bearing requests, normalizing each to `HttpRequest` (lowercase
1184
- * headers, body read as UTF-8 string). Returns the adapter object
1185
- * expected by `CmcdReportRecorder`.
1186
- *
1187
- * Bodies are read once via `Request.text()` for inspection. This is
1188
- * safe for the body types CMCD reports use in practice (`string`,
1189
- * `Blob`, `ArrayBuffer`, `FormData`, `URLSearchParams`), which can be
1190
- * read by the wrapper and re-read by the underlying `fetch`. Passing
1191
- * a `ReadableStream` as `init.body` is not supported — the stream is
1192
- * 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
1193
1261
  * already-disturbed stream.
1194
1262
  *
1195
1263
  * @public
@@ -1255,9 +1323,8 @@ async function completeXhrWith(xhr, response) {
1255
1323
  });
1256
1324
  }
1257
1325
  /**
1258
- * Create a transport adapter that patches `XMLHttpRequest.prototype` to
1259
- * capture CMCD-bearing requests. Returns the adapter object expected by
1260
- * `CmcdReportRecorder`.
1326
+ * Create a transport adapter for `CmcdReportRecorder` that patches
1327
+ * `XMLHttpRequest.prototype` to capture requests with CMCD data.
1261
1328
  *
1262
1329
  * @public
1263
1330
  */
@@ -1320,10 +1387,10 @@ function detectReportingMode(url, headers, isEventTarget) {
1320
1387
  return url.includes(`${CMCD_PARAM}=`) ? "query" : null;
1321
1388
  }
1322
1389
  /**
1323
- * Test helper that records CMCD-bearing reports across XHR and fetch
1324
- * transports for assertion in e2e tests. Each captured request is
1325
- * normalized to {@link @svta/cml-utils!HttpRequest | HttpRequest} so
1326
- * 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.
1327
1394
  *
1328
1395
  * @example
1329
1396
  * {@includeCode ../test/CmcdReportRecorder.test.ts#example}
@@ -1391,11 +1458,11 @@ var CmcdReportRecorder = class {
1391
1458
  });
1392
1459
  }
1393
1460
  /**
1394
- * Install transport patches and begin recording CMCD reports.
1395
- * No-op if already attached — the previously supplied options are
1396
- * retained and the new `options` argument is silently ignored.
1397
- * Call `detach()` first if you need to re-attach with different
1398
- * 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.
1399
1466
  *
1400
1467
  * @public
1401
1468
  */
@@ -1436,7 +1503,7 @@ var CmcdReportRecorder = class {
1436
1503
  this.#reports = [];
1437
1504
  }
1438
1505
  /**
1439
- * Return a defensive copy of all recorded reports.
1506
+ * Return a copy of all recorded reports.
1440
1507
  *
1441
1508
  * @public
1442
1509
  */
@@ -1445,8 +1512,8 @@ var CmcdReportRecorder = class {
1445
1512
  }
1446
1513
  /**
1447
1514
  * Wait until at least `count` reports of any type are recorded.
1448
- * Resolves with all matching reports; rejects with a diagnostic error
1449
- * 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
1450
1517
  * recorder's `waitTimeout` option (default 15000 ms).
1451
1518
  *
1452
1519
  * @public
@@ -1456,8 +1523,8 @@ var CmcdReportRecorder = class {
1456
1523
  }
1457
1524
  /**
1458
1525
  * Wait until at least `count` manifest reports are recorded.
1459
- * Resolves with all matching reports; rejects with a diagnostic error
1460
- * 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
1461
1528
  * recorder's `waitTimeout` option (default 15000 ms).
1462
1529
  *
1463
1530
  * @public
@@ -1467,8 +1534,8 @@ var CmcdReportRecorder = class {
1467
1534
  }
1468
1535
  /**
1469
1536
  * Wait until at least `count` segment reports are recorded.
1470
- * Resolves with all matching reports; rejects with a diagnostic error
1471
- * 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
1472
1539
  * recorder's `waitTimeout` option (default 15000 ms).
1473
1540
  *
1474
1541
  * @public
@@ -1478,8 +1545,8 @@ var CmcdReportRecorder = class {
1478
1545
  }
1479
1546
  /**
1480
1547
  * Wait until at least `count` event-mode reports are recorded.
1481
- * Resolves with all matching reports; rejects with a diagnostic error
1482
- * 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
1483
1550
  * recorder's `waitTimeout` option (default 15000 ms).
1484
1551
  *
1485
1552
  * @public
@@ -1527,12 +1594,12 @@ const CmcdTransmissionMode = {
1527
1594
  //#endregion
1528
1595
  //#region src/upConvertToV2.ts
1529
1596
  /**
1530
- * Up-convert V1 CMCD data to V2 format.
1597
+ * Up-convert version 1 CMCD data to version 2.
1531
1598
  *
1532
- * - Wraps plain scalar values in arrays for inner-list keys.
1599
+ * - Wraps scalar values in arrays for inner-list keys.
1533
1600
  * - Wraps `nor` string in an array.
1534
1601
  *
1535
- * If the data is already V2 (has `v: 2`), it is returned unchanged.
1602
+ * The function returns version 2 data (has `v: 2`) unchanged.
1536
1603
  *
1537
1604
  * @internal
1538
1605
  */
@@ -1581,12 +1648,11 @@ function createEncodingOptions(reportingMode, config, baseUrl) {
1581
1648
  };
1582
1649
  }
1583
1650
  /**
1584
- * Deep equality for CmcdObjectTypeList (used for `br` dedup).
1651
+ * Deep equality for CmcdObjectTypeList (used for `br` deduplication).
1585
1652
  *
1586
- * Order-sensitive: arrays with the same elements in different positions
1587
- * are treated as different. Players that construct `br` consistently
1588
- * get correct dedup; shuffling produces spurious emits, which is the
1589
- * 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.
1590
1656
  */
1591
1657
  function cmcdObjectTypeListEqual(a, b) {
1592
1658
  if (a === b) return true;
@@ -1627,26 +1693,30 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
1627
1693
  };
1628
1694
  });
1629
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
+ }
1630
1704
  /**
1631
1705
  * Maps each event type to the key CTA-5004-B requires beyond `e` and `ts`.
1632
1706
  * Built from the state-change table plus the three event types whose
1633
- * required key rides the caller's per-event data.
1707
+ * required key is event data, not player state.
1634
1708
  */
1635
- const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ new Map([
1636
- ...CMCD_STATE_EVENT_FIELDS,
1637
- [CMCD_EVENT_CUSTOM_EVENT, "cen"],
1638
- [CMCD_EVENT_ERROR, "ec"],
1639
- [CMCD_EVENT_RESPONSE_RECEIVED, "url"]
1640
- ]);
1709
+ const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ buildRequiredEventKeys();
1641
1710
  /**
1642
1711
  * Whether a required key's value will survive report preparation.
1643
1712
  *
1644
- * This is `isValid` minus its `false` exclusion, plus an empty-array check.
1645
- * `false` must count as usable because `bg: false` is a legitimate value on a
1646
- * backgrounded-mode event, which the encoder emits as `?0`; treating it as
1647
- * unusable would let restoration silently revert a transform that cleared it.
1648
- * Empty strings, empty lists and non-finite numbers are dropped downstream, so
1649
- * 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.
1650
1720
  */
1651
1721
  function isUsableRequiredValue(value) {
1652
1722
  if (value == null || value === "") return false;
@@ -1654,12 +1724,12 @@ function isUsableRequiredValue(value) {
1654
1724
  return !Array.isArray(value) || value.length > 0;
1655
1725
  }
1656
1726
  /**
1657
- * Copies an `SfItem`-shaped value and its `params` record.
1727
+ * Copies a value with the `SfItem` structure, including its `params` record.
1658
1728
  *
1659
- * The prototype is preserved because `prepareCmcdData`, the formatter map,
1729
+ * The copy keeps the prototype because `prepareCmcdData`, the formatter map,
1660
1730
  * validation, and the structured-field encoder all branch on
1661
1731
  * `instanceof SfItem`. A plain spread (and `structuredClone`) would return a
1662
- * prototype-less object and silently change what goes on the wire.
1732
+ * prototype-less object and silently change the encoded output.
1663
1733
  */
1664
1734
  function copyItemValue(value) {
1665
1735
  if (value === null || typeof value !== "object") return value;
@@ -1668,16 +1738,16 @@ function copyItemValue(value) {
1668
1738
  return copy;
1669
1739
  }
1670
1740
  /**
1671
- * Copies the nested values of a report in place so a transform cannot mutate
1672
- * the reporter's persistent data, or another target's report for the same
1673
- * 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.
1674
1744
  *
1675
- * Complete for the CMCD value space rather than best-effort: `CmcdValue` and
1745
+ * The copy is complete for the CMCD value space. `CmcdValue` and
1676
1746
  * `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
1677
- * those, and `SfItem.params` is a flat record. Called where a transform is
1678
- * configured, on the ended session's store at archival (detaching the frozen
1679
- * snapshot from caller-held references), and on the request's stored
1680
- * 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.
1681
1751
  */
1682
1752
  function copyReportValues(data) {
1683
1753
  const record = data;
@@ -1692,14 +1762,13 @@ function copyReportValues(data) {
1692
1762
  return data;
1693
1763
  }
1694
1764
  /**
1695
- * Decodes the per-call data snapshot a provenance record carries into
1696
- * fresh, encodable report data: tokens revive as `SfToken` and
1697
- * params-bearing items as `SfItem` (`useSymbol: false`), so every value
1698
- * keeps its wire type through re-encoding. Only called in
1699
- * `recordResponseReceived()`. Returns an empty object when the record is
1700
- * absent, carries no snapshot, or the snapshot does not parse (a value
1701
- * this reporter did not write); the response then reports its derived keys
1702
- * 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.
1703
1772
  */
1704
1773
  function decodeSnapshot(provenance) {
1705
1774
  const encoded = provenance !== null && typeof provenance === "object" ? provenance.data : void 0;
@@ -1711,10 +1780,10 @@ function decodeSnapshot(provenance) {
1711
1780
  }
1712
1781
  }
1713
1782
  /**
1714
- * Mints a session's frozen base provenance record: the issuing `sid`, and
1715
- * the `cid` in effect at mint time. `update()` re-mints on every `cid`
1716
- * change, so requests issued before a mid-session content change keep the
1717
- * `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.
1718
1787
  */
1719
1788
  function mintProvenance(sid, cid) {
1720
1789
  return Object.freeze(typeof cid === "string" && cid ? {
@@ -1753,12 +1822,12 @@ function createCmcdReporterConfig(config) {
1753
1822
  /**
1754
1823
  * The CMCD reporter.
1755
1824
  *
1756
- * `C` describes the player's own `customData`, which the reporter passes
1757
- * through to each configured `transform`. It is inferred from the
1758
- * configuration, so annotating a single `transform` types the request in every
1759
- * 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`.
1760
1829
  *
1761
- * @typeParam C - The shape of the player's `customData`. Defaults to
1830
+ * @typeParam C - The type of the player's `customData`. Defaults to
1762
1831
  * `Record<string, unknown>`.
1763
1832
  *
1764
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}
@@ -1770,7 +1839,7 @@ var CmcdReporter = class {
1770
1839
  * Creates a new CMCD reporter.
1771
1840
  *
1772
1841
  * @param config - The configuration for the CMCD reporter.
1773
- * @param requester - The function to use to send the request.
1842
+ * @param requester - The function that sends the request.
1774
1843
  * The default is a simple wrapper around the
1775
1844
  * native `fetch` API.
1776
1845
  */
@@ -1788,8 +1857,8 @@ var CmcdReporter = class {
1788
1857
  this.requester = requester;
1789
1858
  }
1790
1859
  /**
1791
- * Creates the state for a new session: fresh counters, gates, queues and
1792
- * 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.
1793
1862
  */
1794
1863
  createSession(sid, data) {
1795
1864
  const eventTargets = /* @__PURE__ */ new Map();
@@ -1813,11 +1882,12 @@ var CmcdReporter = class {
1813
1882
  };
1814
1883
  }
1815
1884
  /**
1816
- * 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.
1817
1887
  *
1818
- * Note: This fires an initial time-interval event immediately (synchronously)
1819
- * before the first interval elapses. Ensure CMCD data (sid, cid, etc.) is
1820
- * 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().
1821
1891
  */
1822
1892
  start() {
1823
1893
  this.started = true;
@@ -1836,9 +1906,9 @@ var CmcdReporter = class {
1836
1906
  if (failure) throw failure.error;
1837
1907
  }
1838
1908
  /**
1839
- * Arms the time-interval timer for an event target when its config calls
1840
- * for one. Returns whether a timer was armed. The callback reports into
1841
- * 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.
1842
1912
  */
1843
1913
  armInterval(config) {
1844
1914
  if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return false;
@@ -1853,7 +1923,8 @@ var CmcdReporter = class {
1853
1923
  return true;
1854
1924
  }
1855
1925
  /**
1856
- * 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.
1857
1928
  *
1858
1929
  * @param flush - Whether to flush the event targets.
1859
1930
  */
@@ -1863,8 +1934,8 @@ var CmcdReporter = class {
1863
1934
  for (const config of this.config.eventTargets) this.disarmInterval(config);
1864
1935
  }
1865
1936
  /**
1866
- * Forces the sending of all event reports, regardless of the batch size or interval.
1867
- * 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
1868
1939
  * session ends.
1869
1940
  */
1870
1941
  flush() {
@@ -1873,40 +1944,38 @@ var CmcdReporter = class {
1873
1944
  /**
1874
1945
  * Updates the CMCD data.
1875
1946
  *
1876
- * Called by the player when data changes. For tracked state fields
1877
- * (`sta`, `pr`, `cid`, `bg`, `br`), if the new value differs from the
1878
- * last-reported value (the value most recently emitted on the wire for
1879
- * that field), the corresponding state-change event is automatically
1880
- * fired. Comparing against the last-reported value (rather than the
1881
- * previous persisted value) ensures the first state-change event in a
1882
- * new session always emits, even when the persisted value didn't change
1883
- * 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.
1884
1954
  *
1885
- * Multi-field updates fire multiple events in the order: `sta` → `pr` →
1886
- * `cid` → `bg` → `br`. The order of keys in the input object does not
1887
- * 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.
1888
1958
  *
1889
- * To attach snapshot context (e.g., `bl`, `mtp`, `pt`, `ltc`) to a
1890
- * state-change event, ensure those fields are in the reporter's data
1891
- * before the state field changes. Either include them alongside the
1892
- * state field in the same `update()` call, or persist them via earlier
1893
- * `update()` calls — auto-fired events emit whatever is currently in
1894
- * the persistent data store. This is also how to keep `TIME_INTERVAL`
1895
- * reports useful — those events draw from the persistent data store
1896
- * with no caller hook for per-event data, so fields the player wants
1897
- * 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.
1898
1967
  *
1899
- * A `sid` change resets the dedup baseline.
1968
+ * A `sid` change resets the deduplication baseline.
1900
1969
  *
1901
- * `sid` and `msd` are session-owned: the reporter tracks them itself and
1902
- * 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
1903
1972
  * to change them. A per-call value on {@link CmcdReporter.recordEvent},
1904
1973
  * {@link CmcdReporter.createRequestReport}, or
1905
- * {@link CmcdReporter.recordResponseReceived} has no effect; response
1906
- * attribution is by the provenance record alone.
1974
+ * {@link CmcdReporter.recordResponseReceived} has no effect. Response
1975
+ * attribution uses the provenance record alone.
1907
1976
  * `msd` must be a finite number of milliseconds between `0` and
1908
- * `999_999_999_999_999` (the RFC 8941 integer maximum); it is rounded to
1909
- * 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.
1910
1979
  *
1911
1980
  * @param data - The data to update.
1912
1981
  */
@@ -1928,19 +1997,20 @@ var CmcdReporter = class {
1928
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);
1929
1998
  }
1930
1999
  /**
1931
- * Starts a new session: the current one is archived in place for stale
1932
- * attribution, a fresh one takes over, ended sessions are drained, and
1933
- * 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.
1934
2004
  *
1935
- * The data store is carried over, and the archived snapshot is frozen:
1936
- * the ended session's value graph is detached first, so nested values
1937
- * the caller handed to `update()` can no longer mutate it, and the new
1938
- * session starts from a shallow copy of the detached graph, so `cid`,
1939
- * `br`, custom keys and the rest survive a `sid` change as they always
1940
- * have. Sessions are keyed by `sid`, which CTA-5004-B expects to be
1941
- * unique per playback session: reusing one replaces the retained
1942
- * namesake, whose remaining state dies as if evicted and whose late
1943
- * 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.
1944
2014
  */
1945
2015
  startSession(sid) {
1946
2016
  this.session.data = copyReportValues({ ...this.session.data });
@@ -1957,59 +2027,57 @@ var CmcdReporter = class {
1957
2027
  }
1958
2028
  }
1959
2029
  /**
1960
- * Records an event. Called by the player when an event occurs.
2030
+ * Records an event. The player calls this method when an event occurs.
1961
2031
  *
1962
2032
  * For state-change events (`PLAY_STATE`, `PLAYBACK_RATE`, `CONTENT_ID`,
1963
2033
  * `BACKGROUNDED_MODE`, `BITRATE_CHANGE`), this method:
1964
- * 1. Persists the dedup field from `data` (if present) into the reporter's
1965
- * persistent data store — equivalent to a write-through `update()`.
1966
- * 2. Drops the event entirely if the dedup field has no value after the
1967
- * write-through (never set, or cleared via `update({ field: undefined })`).
1968
- * State-change events without their required field would violate CTA-5004-B.
1969
- * 3. Suppresses the event if the field's current value matches the
1970
- * 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).
1971
2041
  *
1972
- * For all other event types, the event is always emitted.
2042
+ * The reporter always records all other event types.
1973
2043
  *
1974
- * For state-change events, prefer {@link CmcdReporter.update} — including
1975
- * for snapshot enrichment via a combined call like
1976
- * `update({ sta: 'p', bl: [3000], mtp: [8500] })`. Calling `recordEvent()`
1977
- * for a state-change event after `update()` has already auto-fired it
1978
- * silently drops the second call's `data`, because dedup suppresses the
1979
- * 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.
1980
2050
  *
1981
- * Use `recordEvent()` directly for events whose payload is intrinsic to
1982
- * the event call — `CUSTOM_EVENT` with `cen`, `ERROR` with `ec`,
1983
- * ad-lifecycle events, `MUTE`/`UNMUTE`, `PLAYER_EXPAND`/`PLAYER_COLLAPSE`,
1984
- * `SKIP`. For `RESPONSE_RECEIVED`, prefer
1985
- * {@link CmcdReporter.recordResponseReceived}, which derives the
1986
- * 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.
1987
2056
  *
1988
2057
  * @param type - The type of event to record.
1989
- * @param data - Additional data to record with the event. This data
1990
- * only applies to this event report, except for the dedup
1991
- * field of a state-change event, which is also persisted
1992
- * into the reporter's data store. Session-owned keys
1993
- * (`sid`, `msd`) supplied here are ignored; the reporter
1994
- * 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.
1995
2063
  */
1996
2064
  recordEvent(type, data = {}) {
1997
2065
  this.emitEvent(this.session, type, data);
1998
2066
  }
1999
2067
  /**
2000
- * Records an event across every configured target of a session, applying
2001
- * 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.
2002
2070
  *
2003
2071
  * @param session - The session the event belongs to. Everything the event
2004
- * touches (data store, dedup baseline, counters, queues)
2005
- * is that session's; only `recordResponseReceived()` ever
2006
- * 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.
2007
2075
  * @param type - The type of event to record.
2008
2076
  * @param data - Additional data to record with the event.
2009
2077
  * @param request - The media request that triggered the event, when
2010
- * one exists. Only `recordResponseReceived()`
2011
- * populates this; it is threaded through to each
2012
- * target's `transform`.
2078
+ * one exists. Only `recordResponseReceived()` sets this
2079
+ * parameter, which the reporter passes to each target's
2080
+ * `transform`.
2013
2081
  */
2014
2082
  emitEvent(session, type, data, request) {
2015
2083
  const entry = STATE_FIELDS_BY_EVENT.get(type);
@@ -2034,16 +2102,17 @@ var CmcdReporter = class {
2034
2102
  if (failure) throw failure.error;
2035
2103
  }
2036
2104
  /**
2037
- * 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.
2038
2107
  *
2039
2108
  * @param target - The target to record the event for.
2040
2109
  * @param config - The configuration for the target.
2041
2110
  * @param type - The type of event to record.
2042
2111
  * @param data - Additional data to record with the event. This data
2043
- * only applies to this event report. Persistent data should
2044
- * be updated using `update()`.
2112
+ * only applies to this event report. Use `update()` for
2113
+ * persistent data.
2045
2114
  * @param request - The media request that triggered the event, when
2046
- * one exists. Passed to the target's `transform`.
2115
+ * one exists. The target's `transform` receives it.
2047
2116
  */
2048
2117
  recordTargetEvent(session, target, config, type, data = {}, request) {
2049
2118
  if (target.disposed || !config.events.includes(type)) return;
@@ -2070,15 +2139,14 @@ var CmcdReporter = class {
2070
2139
  this.queueTargetEvent(session, target, config, report, type);
2071
2140
  }
2072
2141
  /**
2073
- * Stamps the reporter-owned fields on a finished event report, encodes it,
2074
- * 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.
2075
2144
  *
2076
- * Called after any transform has run, so a transform cannot bypass the
2077
- * target's `events` filter via `e`, break `sn` continuity, or substitute
2078
- * the session identity carried by `sid` and `msd`. Encoding here means a
2079
- * report that cannot serialize throws inside the recording call that
2080
- * produced it, instead of rejecting the batch send and re-queueing
2081
- * 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.
2082
2150
  *
2083
2151
  * @param target - The target to queue the report for.
2084
2152
  * @param config - The configuration for the target.
@@ -2096,10 +2164,10 @@ var CmcdReporter = class {
2096
2164
  target.queue.push(encodeCmcd(report, createEncodingOptions(CMCD_EVENT_MODE, config)));
2097
2165
  }
2098
2166
  /**
2099
- * Records a response-received event. Called by the player when a media
2100
- * 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.
2101
2169
  *
2102
- * This method automatically derives the `rr` event keys from the
2170
+ * This method derives these `rr` event keys automatically:
2103
2171
  *
2104
2172
  * - `url` - the original requested URL (before any redirects)
2105
2173
  * - `rc` - the HTTP response status code
@@ -2107,50 +2175,50 @@ var CmcdReporter = class {
2107
2175
  * - `ttfb` - time to first byte (from `resourceTiming.responseStart`)
2108
2176
  * - `ttlb` - time to last byte (from `resourceTiming.duration`)
2109
2177
  *
2110
- * Additional keys like `ttfbb`, `cmsdd`, `cmsds`, and `smrt` can be
2111
- * 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.
2112
2180
  *
2113
- * The request's `customData` is generic, so a player can pass a request
2114
- * carrying its own taxonomy (e.g. `{ requestType: 'segment' }`) without a
2115
- * cast. The reporter reads only the provenance record that
2116
- * {@link CmcdReporter.createRequestReport} writes there; every other key
2117
- * is left untouched and stays visible to an event target's `transform`
2118
- * 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.
2119
2187
  *
2120
- * A reporter given a concrete `C` requires the request to satisfy it, so a
2121
- * request the configured transforms could not read is rejected here rather
2122
- * 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}.
2123
2191
  *
2124
- * The event is attributed to the session that issued the request, and to
2125
- * 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
2126
2194
  * {@link CmcdReporter.createRequestReport} stored on the request's
2127
- * `customData` (under {@link CMCD_REQUEST_PROVENANCE}) selects the
2128
- * session by its `sid`, or the response is dropped rather than
2129
- * relabeled. There is no other key: a record lost to a serialization
2130
- * boundary (and not restored; see the bridge notes on
2131
- * {@link CMCD_REQUEST_PROVENANCE}) and a session no longer retained
2132
- * (see `CmcdReporterConfig.sessionRetention`) both drop, and a per-call
2133
- * `data.sid` cannot substitute. The record is honored wherever its
2134
- * `sid` resolves: a request decorated by another reporter configured
2135
- * with the same session attributes here, as does a hand-built record.
2136
- * A response that completes after a `sid` change reports under its own
2137
- * retained session, with that session's data snapshot and sequence
2138
- * 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.
2139
2207
  *
2140
- * Request-time report keys come from the record's encoded per-call
2141
- * `data` snapshot, decoded fresh per response, and the record's `cid`
2142
- * reports in place of the session's current one, so a response that
2143
- * completes after a mid-session content change keeps the meaning it
2144
- * 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.
2145
2213
  *
2146
- * @typeParam RD - The `customData` this request carries. Defaults to the
2214
+ * @typeParam RD - The `customData` of this request. Defaults to the
2147
2215
  * reporter's own `C`.
2148
2216
  *
2149
2217
  * @param response - The HTTP response received.
2150
2218
  * @param data - Additional CMCD data to include with the event.
2151
- * Values provided here override any auto-derived values.
2152
- * Session-owned keys (`sid`, `msd`) supplied here are
2153
- * 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.
2154
2222
  */
2155
2223
  recordResponseReceived(response, data = {}) {
2156
2224
  const { request } = response;
@@ -2183,13 +2251,13 @@ var CmcdReporter = class {
2183
2251
  }, request);
2184
2252
  }
2185
2253
  /**
2186
- * Resolves the session a response belongs to: the provenance record's
2187
- * `sid` names one of this reporter's retained sessions, or the response
2188
- * is dropped. There is no other key — a record that was lost, or that
2189
- * names an evicted or never-seen `sid`, resolves nothing, and
2190
- * attributing it anywhere else would relabel it. The `sid` is read
2191
- * structurally, so a JSON-revived copy of a record attributes exactly,
2192
- * 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.
2193
2261
  */
2194
2262
  resolveSession(provenance) {
2195
2263
  if (provenance === null || typeof provenance !== "object") return;
@@ -2197,8 +2265,8 @@ var CmcdReporter = class {
2197
2265
  return typeof sid === "string" ? this.sessions.get(sid) : void 0;
2198
2266
  }
2199
2267
  /**
2200
- * Applies the CMCD request report data to the request. Called by the player
2201
- * before sending the request.
2268
+ * Applies the CMCD request report data to the request. The player calls this
2269
+ * method before sending the request.
2202
2270
  *
2203
2271
  * @param req - The request to apply the CMCD request report to.
2204
2272
  * @returns The request with the CMCD request report applied.
@@ -2209,23 +2277,22 @@ var CmcdReporter = class {
2209
2277
  return this.createRequestReport(req) ?? req;
2210
2278
  }
2211
2279
  /**
2212
- * Checks if the request reporting is enabled.
2280
+ * Checks whether request reporting is enabled.
2213
2281
  *
2214
- * @returns `true` if the request reporting is enabled, `false` otherwise.
2282
+ * @returns `true` if request reporting is enabled, `false` otherwise.
2215
2283
  */
2216
2284
  isRequestReportingEnabled() {
2217
2285
  return !!this.config.enabledKeys?.length;
2218
2286
  }
2219
2287
  /**
2220
- * The provenance record to stamp on a returned request: the session's
2221
- * frozen base record, extended with the caller's per-call data encoded
2222
- * as a CMCD string when there is any. Requests without per-call data
2223
- * share the base record, so decoration stays a single property write.
2224
- * The snapshot never contains reporter-stamped fields, and a value that
2225
- * cannot encode contributes no snapshot rather than failing the
2226
- * request; its response still attributes and reports derived keys over
2227
- * session data, matching `decodeSnapshot()`'s tolerance on the read
2228
- * 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.
2229
2296
  */
2230
2297
  createRequestProvenance(data, baseUrl) {
2231
2298
  const base = this.session.provenance;
@@ -2245,21 +2312,21 @@ var CmcdReporter = class {
2245
2312
  }
2246
2313
  }
2247
2314
  /**
2248
- * Creates a new request with the CMCD request report data applied. Called by the player
2249
- * 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.
2250
2317
  *
2251
- * A reporter given a concrete `C` requires the request's `customData` to
2252
- * satisfy it, so a request the configured transform could not read is
2253
- * 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
2254
2321
  * {@link CmcdReporterCustomData}.
2255
2322
  *
2256
- * @typeParam R - The request being decorated. Its `customData` must satisfy
2323
+ * @typeParam R - The type of the request. Its `customData` must satisfy
2257
2324
  * the reporter's `C`.
2258
2325
  *
2259
2326
  * @param request - The request to apply the CMCD request report to.
2260
2327
  * @param data - The data to apply to the request. This data only
2261
- * applies to this request report. Persistent data
2262
- * should be updated using `update()`.
2328
+ * applies to this request report. Use `update()` for
2329
+ * persistent data.
2263
2330
  * @returns The request with the CMCD request report applied.
2264
2331
  */
2265
2332
  createRequestReport(request, data) {
@@ -2311,7 +2378,8 @@ var CmcdReporter = class {
2311
2378
  return report;
2312
2379
  }
2313
2380
  /**
2314
- * 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.
2315
2383
  *
2316
2384
  * @param flush - Whether to flush the event targets.
2317
2385
  */
@@ -2334,7 +2402,8 @@ var CmcdReporter = class {
2334
2402
  if (reprocess) this.processEventTargets();
2335
2403
  }
2336
2404
  /**
2337
- * 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.
2338
2407
  *
2339
2408
  * @param config - The target config to send the event report to.
2340
2409
  * @param data - The encoded report lines to send in the event report.
@@ -2344,27 +2413,27 @@ var CmcdReporter = class {
2344
2413
  url: config.url,
2345
2414
  method: "POST",
2346
2415
  headers: { "Content-Type": CMCD_MIME_TYPE },
2347
- body: data.join("\n") + "\n"
2416
+ body: data.join("\n")
2348
2417
  });
2349
2418
  if (status === 410) this.disposeEventTarget(session, config);
2350
2419
  else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
2351
2420
  }
2352
2421
  /**
2353
2422
  * Cancels the time-interval timer for an event target config and clears the
2354
- * stored id. Safe to call when no timer is armed (clearInterval(undefined)
2355
- * is a no-op).
2423
+ * stored id. Safe to call when no timer is active (clearInterval(undefined)
2424
+ * does nothing).
2356
2425
  */
2357
2426
  disarmInterval(config) {
2358
2427
  clearInterval(this.intervals.get(config));
2359
2428
  this.intervals.delete(config);
2360
2429
  }
2361
2430
  /**
2362
- * Silences an event target URL for the remainder of its session: drops the
2363
- * queues and blocks further enqueues for every config in the session that
2364
- * reports to the URL, per CTA-5004-B's per-target-URL suppression scope,
2365
- * and cancels their timers when the session is the live one. Used when the
2366
- * collector signals the target is gone (HTTP 410). A session started after
2367
- * 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.
2368
2437
  */
2369
2438
  disposeEventTarget(session, config) {
2370
2439
  session.eventTargets.forEach((target, sibling) => {
@@ -2517,7 +2586,7 @@ function isCmcdV2Data(data) {
2517
2586
  * Convert a value to a CMCD value.
2518
2587
  *
2519
2588
  * @param value - The value to convert to a CMCD value.
2520
- * @param params - The parameters to convert to a CMCD value.
2589
+ * @param params - The parameters of the CMCD value.
2521
2590
  * @returns The CMCD value.
2522
2591
  *
2523
2592
  * @public
@@ -2544,7 +2613,7 @@ function mergeValidationResults(...results) {
2544
2613
  //#endregion
2545
2614
  //#region src/resolveVersion.ts
2546
2615
  /**
2547
- * 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).
2548
2617
  *
2549
2618
  * @internal
2550
2619
  */
@@ -2669,6 +2738,23 @@ function validateCmcdStructure(data, options) {
2669
2738
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2670
2739
  });
2671
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
+ }
2672
2758
  if ("v" in data && data["v"] !== 1 && data["v"] !== 2) issues.push({
2673
2759
  key: "v",
2674
2760
  message: `Unsupported CMCD version "${String(data["v"])}". Expected 1 or 2.`,
@@ -2735,8 +2821,8 @@ const CMCD_KEY_TYPE_STRING = "string";
2735
2821
  */
2736
2822
  const CMCD_KEY_TYPE_TOKEN = "token";
2737
2823
  /**
2738
- * Maps each CMCD spec key to its expected value type for v2.
2739
- * 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.
2740
2826
  *
2741
2827
  * @internal
2742
2828
  */
@@ -2794,7 +2880,7 @@ const CMCD_KEY_TYPES = {
2794
2880
  sta: CMCD_KEY_TYPE_TOKEN
2795
2881
  };
2796
2882
  /**
2797
- * Maps keys to their v1-specific types when they differ from v2.
2883
+ * Version 1 types of the keys that differ from version 2.
2798
2884
  *
2799
2885
  * @internal
2800
2886
  */
@@ -3098,8 +3184,8 @@ function validateCmcd(data, options) {
3098
3184
  *
3099
3185
  * This function decodes the string internally and validates it with
3100
3186
  * `reportingMode` set to `'event'`. The input may contain multiple
3101
- * newline-separated events (e.g. an `application/cmcd` POST body), in which
3102
- * 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.
3103
3189
  *
3104
3190
  * @param cmcd - The raw CMCD-encoded string to validate. May contain
3105
3191
  * multiple newline-separated event lines.
@@ -3127,6 +3213,11 @@ function validateCmcdEvents(cmcd, options) {
3127
3213
  }],
3128
3214
  data: []
3129
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
+ });
3130
3221
  const decodedLines = [];
3131
3222
  const lineResults = [];
3132
3223
  for (let i = 0; i < lines.length; i++) try {
@@ -3144,7 +3235,10 @@ function validateCmcdEvents(cmcd, options) {
3144
3235
  });
3145
3236
  }
3146
3237
  return {
3147
- ...mergeValidationResults(...lineResults),
3238
+ ...mergeValidationResults({
3239
+ valid: true,
3240
+ issues: bodyIssues
3241
+ }, ...lineResults),
3148
3242
  data: decodedLines
3149
3243
  };
3150
3244
  }
@@ -3157,8 +3251,8 @@ function validateCmcdEvents(cmcd, options) {
3157
3251
  * Accepts an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
3158
3252
  *
3159
3253
  * This function validates that the request uses the POST method and has
3160
- * the correct `Content-Type` header (`application/cmcd`) in addition to
3161
- * 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}.
3162
3256
  *
3163
3257
  * @param request - An `HttpRequest` to validate.
3164
3258
  * @param options - Validation options (excluding `reportingMode`).
@@ -3224,15 +3318,15 @@ function validateCmcdEventReport(request, options) {
3224
3318
  /**
3225
3319
  * Validates CMCD HTTP headers by checking shard placement and payload validity.
3226
3320
  *
3227
- * This function accepts raw CMCD header strings, decodes each shard
3228
- * internally, verifies that each key is placed in its correct header shard,
3229
- * then merges all shards and runs full payload validation (keys, values, and
3230
- * 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.
3231
3325
  *
3232
3326
  * @example
3233
3327
  * {@includeCode ../test/validateCmcdHeaders.test.ts#example}
3234
3328
  *
3235
- * @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.
3236
3330
  * @param options - Validation options (excluding `reportingMode`).
3237
3331
  * @returns The validation result including decoded data.
3238
3332
  *
@@ -3292,10 +3386,12 @@ function validateCmcdHeaders(headers, options) {
3292
3386
  * {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request}
3293
3387
  * object or an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
3294
3388
  *
3295
- * The function checks for CMCD data in the HTTP headers first. If CMCD
3296
- * headers are found, validation includes shard-placement checks via
3297
- * {@link validateCmcdHeaders}. Otherwise, the CMCD query parameter is
3298
- * 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.
3299
3395
  *
3300
3396
  * @param request - A `Request` or `HttpRequest` to validate.
3301
3397
  * @param options - Validation options (excluding `reportingMode`).
@@ -3310,12 +3406,25 @@ function validateCmcdHeaders(headers, options) {
3310
3406
  */
3311
3407
  function validateCmcdRequest(request, options) {
3312
3408
  const headers = extractHeaderRecord(request.headers);
3313
- if (headers) return validateCmcdHeaders(headers, options);
3314
- 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
+ }
3315
3424
  if (!param) return {
3316
3425
  valid: false,
3317
3426
  issues: [{
3318
- 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.",
3319
3428
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3320
3429
  }],
3321
3430
  data: {}
@@ -3341,6 +3450,18 @@ function validateCmcdRequest(request, options) {
3341
3450
  data
3342
3451
  };
3343
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
+ }
3344
3465
  function extractHeaderRecord(headers) {
3345
3466
  if (!headers) return;
3346
3467
  const h = ensureHeaders(headers);