@svta/cml-cmcd 2.6.1 → 2.8.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,229 +1,325 @@
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_FORMATTER_MAP.ts
5
- const roundValue = (value) => {
6
- if (value instanceof SfItem) return new SfItem(Math.round(value.value), value.params);
7
- return Math.round(value);
8
- };
9
- const toRounded = (value) => {
10
- if (Array.isArray(value)) return value.map(roundValue);
11
- return roundValue(value);
12
- };
13
- const toUrlSafe = (value, options) => {
14
- if (Array.isArray(value)) return value.map((item) => toUrlSafe(item, options));
15
- if (value instanceof SfItem && typeof value.value === "string") return new SfItem(toUrlSafe(value.value, options), value.params);
16
- else {
17
- if (options.baseUrl) value = urlToRelativePath(value, getBaseUrl(options.baseUrl));
18
- return options.version === 1 ? encodeURIComponent(value) : value;
19
- }
20
- };
21
- const hundredValue = (value) => {
22
- if (value instanceof SfItem) return new SfItem(Math.round(value.value / 100) * 100, value.params);
23
- return Math.round(value / 100) * 100;
24
- };
25
- const toHundred = (value) => {
26
- if (Array.isArray(value)) return value.map(hundredValue);
27
- return hundredValue(value);
28
- };
29
- const nor = (value, options) => {
30
- let norValue = value;
31
- if (options.version >= 2) {
32
- if (value instanceof SfItem && typeof value.value === "string") norValue = new SfItem([value]);
33
- else if (typeof value === "string") norValue = [value];
34
- }
35
- return toUrlSafe(norValue, options);
36
- };
4
+ //#region src/CMCD_INNER_LIST_KEYS.ts
37
5
  /**
38
- * The default formatters for CMCD values.
6
+ * Inner-list keys in version 2 that are scalars in version 1.
39
7
  *
40
- * @public
41
- */
42
- const CMCD_FORMATTER_MAP = {
43
- br: toRounded,
44
- d: toRounded,
45
- bl: toHundred,
46
- dl: toHundred,
47
- mtp: toHundred,
48
- nor,
49
- rtp: toHundred,
50
- tb: toRounded
51
- };
52
-
53
- //#endregion
54
- //#region src/CMCD_V2.ts
55
- /**
56
- * CMCD Version 2
8
+ * Used by both encoding (down-conversion) and decoding (up-conversion).
57
9
  *
58
- * @public
10
+ * @internal
59
11
  */
60
- const CMCD_V2 = 2;
12
+ const CMCD_INNER_LIST_KEYS = new Set([
13
+ "ab",
14
+ "bl",
15
+ "br",
16
+ "bsa",
17
+ "bsd",
18
+ "bsda",
19
+ "lab",
20
+ "lb",
21
+ "mtp",
22
+ "pb",
23
+ "tab",
24
+ "tb",
25
+ "tbl",
26
+ "tpb"
27
+ ]);
61
28
 
62
29
  //#endregion
63
- //#region src/CmcdEventType.ts
64
- /**
65
- * CMCD event type for the 'bc' key (bitrate change).
66
- *
67
- * @public
68
- */
69
- const CMCD_EVENT_BITRATE_CHANGE = "bc";
70
- /**
71
- * CMCD event type for the 'ps' key (play state change).
72
- *
73
- * @public
74
- */
75
- const CMCD_EVENT_PLAY_STATE = "ps";
76
- /**
77
- * CMCD event type for the 'pr' key (playback rate change).
78
- *
79
- * @public
80
- */
81
- const CMCD_EVENT_PLAYBACK_RATE = "pr";
82
- /**
83
- * CMCD event type for the 'e' key (error).
84
- *
85
- * @public
86
- */
87
- const CMCD_EVENT_ERROR = "e";
88
- /**
89
- * CMCD event type for the 't' key (time interval).
90
- *
91
- * @public
92
- */
93
- const CMCD_EVENT_TIME_INTERVAL = "t";
94
- /**
95
- * CMCD event type for the 'c' key (content ID).
96
- *
97
- * @public
98
- */
99
- const CMCD_EVENT_CONTENT_ID = "c";
100
- /**
101
- * CMCD event type for the 'b' key (backgrounded mode).
102
- *
103
- * @public
104
- */
105
- const CMCD_EVENT_BACKGROUNDED_MODE = "b";
106
- /**
107
- * CMCD event type for the 'm' key (mute).
108
- *
109
- * @public
110
- */
111
- const CMCD_EVENT_MUTE = "m";
112
- /**
113
- * CMCD event type for the 'um' key (unmute).
114
- *
115
- * @public
116
- */
117
- const CMCD_EVENT_UNMUTE = "um";
118
- /**
119
- * CMCD event type for the 'pe' key (player expand).
120
- *
121
- * @public
122
- */
123
- const CMCD_EVENT_PLAYER_EXPAND = "pe";
124
- /**
125
- * CMCD event type for the 'pc' key (player collapse).
126
- *
127
- * @public
128
- */
129
- const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
130
- /**
131
- * CMCD event type for the 'rr' key (response received).
132
- *
133
- * @public
134
- */
135
- const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
136
- /**
137
- * CMCD event type for the 'as' key (ad start).
138
- *
139
- * @public
140
- */
141
- const CMCD_EVENT_AD_START = "as";
142
- /**
143
- * CMCD event type for the 'ae' key (ad end).
144
- *
145
- * @public
146
- */
147
- const CMCD_EVENT_AD_END = "ae";
148
- /**
149
- * CMCD event type for the 'abs' key (ad break start).
150
- *
151
- * @public
152
- */
153
- const CMCD_EVENT_AD_BREAK_START = "abs";
154
- /**
155
- * CMCD event type for the 'abe' key (ad break end).
156
- *
157
- * @public
158
- */
159
- const CMCD_EVENT_AD_BREAK_END = "abe";
160
- /**
161
- * CMCD event type for the 'sk' key (skip).
162
- *
163
- * @public
164
- */
165
- const CMCD_EVENT_SKIP = "sk";
166
- /**
167
- * CMCD event type for the 'ce' key (custom event).
168
- *
169
- * @public
170
- */
171
- const CMCD_EVENT_CUSTOM_EVENT = "ce";
30
+ //#region src/CMCD_KEY_SPECS.ts
31
+ const MEDIA = [
32
+ "a",
33
+ "v",
34
+ "av",
35
+ "tt",
36
+ "c",
37
+ "o"
38
+ ];
172
39
  /**
173
- * CMCD event types for the 'e' key (event mode).
174
- *
175
- * @enum
176
- *
177
- * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-B Event}
40
+ * The encoder rules of each key. The table has one row for each key of CTA-5004-B Table 1, plus `nrr` of CTA-5004 and `cdn`.
178
41
  *
179
- * @public
42
+ * @internal
180
43
  */
181
- const CmcdEventType = {
182
- BITRATE_CHANGE: CMCD_EVENT_BITRATE_CHANGE,
183
- PLAY_STATE: CMCD_EVENT_PLAY_STATE,
184
- PLAYBACK_RATE: CMCD_EVENT_PLAYBACK_RATE,
185
- ERROR: CMCD_EVENT_ERROR,
186
- TIME_INTERVAL: CMCD_EVENT_TIME_INTERVAL,
187
- CONTENT_ID: CMCD_EVENT_CONTENT_ID,
188
- BACKGROUNDED_MODE: CMCD_EVENT_BACKGROUNDED_MODE,
189
- MUTE: CMCD_EVENT_MUTE,
190
- UNMUTE: CMCD_EVENT_UNMUTE,
191
- PLAYER_EXPAND: CMCD_EVENT_PLAYER_EXPAND,
192
- PLAYER_COLLAPSE: CMCD_EVENT_PLAYER_COLLAPSE,
193
- RESPONSE_RECEIVED: CMCD_EVENT_RESPONSE_RECEIVED,
194
- AD_START: CMCD_EVENT_AD_START,
195
- AD_END: CMCD_EVENT_AD_END,
196
- AD_BREAK_START: CMCD_EVENT_AD_BREAK_START,
197
- AD_BREAK_END: CMCD_EVENT_AD_BREAK_END,
198
- SKIP: CMCD_EVENT_SKIP,
199
- CUSTOM_EVENT: CMCD_EVENT_CUSTOM_EVENT
44
+ const CMCD_KEY_SPECS = {
45
+ ab: {
46
+ type: "ot-list",
47
+ round: 1,
48
+ supersededBy: "br",
49
+ version: 2
50
+ },
51
+ bg: {
52
+ type: "boolean",
53
+ omitDefault: false,
54
+ requiredOn: "b",
55
+ version: 2
56
+ },
57
+ bl: {
58
+ type: "ot-list",
59
+ round: 100
60
+ },
61
+ br: {
62
+ type: "ot-list",
63
+ round: 1,
64
+ requiredOn: "bc"
65
+ },
66
+ bs: {
67
+ type: "boolean",
68
+ omitDefault: false
69
+ },
70
+ bsa: {
71
+ type: "ot-list",
72
+ round: 1,
73
+ version: 2
74
+ },
75
+ bsd: {
76
+ type: "ot-list",
77
+ round: 1,
78
+ version: 2
79
+ },
80
+ bsda: {
81
+ type: "ot-list",
82
+ round: 1,
83
+ version: 2
84
+ },
85
+ cdn: {
86
+ type: "string",
87
+ max: 128,
88
+ version: 2
89
+ },
90
+ cen: {
91
+ type: "string",
92
+ max: 64,
93
+ mode: "event",
94
+ onlyOn: "ce",
95
+ requiredOn: "ce",
96
+ version: 2
97
+ },
98
+ cid: {
99
+ type: "string",
100
+ max: 128,
101
+ v1Max: 64,
102
+ requiredOn: "c"
103
+ },
104
+ cmsdd: {
105
+ type: "string",
106
+ mode: "event",
107
+ onlyOn: "rr",
108
+ version: 2
109
+ },
110
+ cmsds: {
111
+ type: "string",
112
+ mode: "event",
113
+ onlyOn: "rr",
114
+ version: 2
115
+ },
116
+ cs: {
117
+ type: "string",
118
+ version: 2
119
+ },
120
+ d: {
121
+ type: "integer",
122
+ round: 1,
123
+ ot: MEDIA
124
+ },
125
+ dfa: {
126
+ type: "integer",
127
+ round: 1,
128
+ version: 2
129
+ },
130
+ dl: {
131
+ type: "integer",
132
+ round: 100
133
+ },
134
+ e: {
135
+ type: "token",
136
+ mode: "event",
137
+ requiredOn: "always",
138
+ version: 2
139
+ },
140
+ ec: {
141
+ type: "string-list",
142
+ requiredOn: "e",
143
+ version: 2
144
+ },
145
+ h: {
146
+ type: "string",
147
+ max: 128,
148
+ mode: "event",
149
+ requiredOn: "h",
150
+ version: 2
151
+ },
152
+ lab: {
153
+ type: "ot-list",
154
+ round: 1,
155
+ supersededBy: "lb",
156
+ version: 2
157
+ },
158
+ lb: {
159
+ type: "ot-list",
160
+ round: 1,
161
+ version: 2
162
+ },
163
+ ltc: {
164
+ type: "integer",
165
+ round: 1,
166
+ version: 2
167
+ },
168
+ msd: {
169
+ type: "integer",
170
+ round: 1,
171
+ version: 2
172
+ },
173
+ mtp: {
174
+ type: "ot-list",
175
+ round: 100
176
+ },
177
+ nor: { type: "nor" },
178
+ nr: {
179
+ type: "boolean",
180
+ omitDefault: false,
181
+ version: 2
182
+ },
183
+ nrr: {
184
+ type: "string",
185
+ version: 1
186
+ },
187
+ ot: { type: "token" },
188
+ pb: {
189
+ type: "ot-list",
190
+ round: 1,
191
+ version: 2
192
+ },
193
+ pr: {
194
+ type: "decimal",
195
+ omitDefault: 1,
196
+ requiredOn: "pr"
197
+ },
198
+ pt: {
199
+ type: "integer",
200
+ round: 1,
201
+ version: 2
202
+ },
203
+ rc: {
204
+ type: "integer",
205
+ round: 1,
206
+ mode: "event",
207
+ onlyOn: "rr",
208
+ version: 2
209
+ },
210
+ rtp: {
211
+ type: "integer",
212
+ round: 100
213
+ },
214
+ sf: {
215
+ type: "token",
216
+ v1Map: { e: "o" }
217
+ },
218
+ sid: {
219
+ type: "string",
220
+ max: 64
221
+ },
222
+ smrt: {
223
+ type: "string",
224
+ mode: "event",
225
+ onlyOn: "rr",
226
+ version: 2
227
+ },
228
+ sn: {
229
+ type: "integer",
230
+ round: 1,
231
+ version: 2
232
+ },
233
+ st: {
234
+ type: "token",
235
+ v1Map: { ll: "l" }
236
+ },
237
+ sta: {
238
+ type: "token",
239
+ requiredOn: "ps",
240
+ version: 2
241
+ },
242
+ su: {
243
+ type: "boolean",
244
+ omitDefault: false
245
+ },
246
+ tab: {
247
+ type: "ot-list",
248
+ round: 1,
249
+ supersededBy: "tb",
250
+ version: 2
251
+ },
252
+ tb: {
253
+ type: "ot-list",
254
+ round: 1
255
+ },
256
+ tbl: {
257
+ type: "ot-list",
258
+ round: 100,
259
+ version: 2
260
+ },
261
+ tpb: {
262
+ type: "ot-list",
263
+ round: 1,
264
+ ot: [
265
+ "a",
266
+ "v",
267
+ "av",
268
+ "c"
269
+ ],
270
+ version: 2
271
+ },
272
+ ts: {
273
+ type: "integer",
274
+ round: 1,
275
+ mode: "event",
276
+ requiredOn: "always",
277
+ version: 2
278
+ },
279
+ ttfb: {
280
+ type: "integer",
281
+ round: 1,
282
+ mode: "event",
283
+ onlyOn: "rr",
284
+ version: 2
285
+ },
286
+ ttfbb: {
287
+ type: "integer",
288
+ round: 1,
289
+ mode: "event",
290
+ onlyOn: "rr",
291
+ version: 2
292
+ },
293
+ ttlb: {
294
+ type: "integer",
295
+ round: 1,
296
+ mode: "event",
297
+ onlyOn: "rr",
298
+ version: 2
299
+ },
300
+ url: {
301
+ type: "string",
302
+ mode: "event",
303
+ onlyOn: "rr",
304
+ requiredOn: "rr",
305
+ version: 2
306
+ },
307
+ v: {
308
+ type: "integer",
309
+ round: 1,
310
+ requiredOn: "always",
311
+ version: 2
312
+ }
200
313
  };
201
314
 
202
315
  //#endregion
203
- //#region src/CMCD_STATE_EVENT_FIELDS.ts
316
+ //#region src/CMCD_V2.ts
204
317
  /**
205
- * Maps each state-change event type to the persistent field whose value
206
- * the event signals.
207
- *
208
- * 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`).
213
- *
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.
216
- * Do not reorder entries without auditing reporter behavior.
318
+ * CMCD Version 2
217
319
  *
218
- * @internal
320
+ * @public
219
321
  */
220
- const CMCD_STATE_EVENT_FIELDS = /* @__PURE__ */ new Map([
221
- [CMCD_EVENT_PLAY_STATE, "sta"],
222
- [CMCD_EVENT_PLAYBACK_RATE, "pr"],
223
- [CMCD_EVENT_CONTENT_ID, "cid"],
224
- [CMCD_EVENT_BACKGROUNDED_MODE, "bg"],
225
- [CMCD_EVENT_BITRATE_CHANGE, "br"]
226
- ]);
322
+ const CMCD_V2 = 2;
227
323
 
228
324
  //#endregion
229
325
  //#region src/CmcdReportingMode.ts
@@ -252,75 +348,68 @@ const CmcdReportingMode = {
252
348
  };
253
349
 
254
350
  //#endregion
255
- //#region src/CMCD_EVENT_KEYS.ts
351
+ //#region src/downConvertToV1.ts
352
+ function hasParams(params) {
353
+ for (const _ in params) return true;
354
+ return false;
355
+ }
256
356
  /**
257
- * Defines the event-specific keys for CMCD (Common Media Client Data) version 2.
357
+ * Unwrap an inner list or SfItem value to a scalar.
258
358
  *
259
- * @public
359
+ * The scalar is the value of the item with the object type flag of `ot`
360
+ * (CTA-5004-B section 4.1, item 14). Without that item, it is the value
361
+ * of the first item without parameters. Otherwise it is `undefined`.
260
362
  */
261
- const CMCD_EVENT_KEYS = [
262
- "cen",
263
- "e",
264
- "h",
265
- "ts"
266
- ];
267
-
268
- //#endregion
269
- //#region src/CMCD_REQUEST_KEYS.ts
363
+ function unwrapValue(value, ot) {
364
+ if (value instanceof SfItem) value = value.value;
365
+ if (!Array.isArray(value)) return value;
366
+ let fallback;
367
+ for (const item of value) {
368
+ if (!(item instanceof SfItem)) {
369
+ if (fallback === void 0) fallback = item;
370
+ continue;
371
+ }
372
+ if (ot && item.params?.[ot] === true) return item.value;
373
+ if (fallback === void 0 && !hasParams(item.params)) fallback = item.value;
374
+ }
375
+ return fallback;
376
+ }
270
377
  /**
271
- * Defines the request-specific keys for CMCD (Common Media Client Data) version 2.
378
+ * Down-convert version 2 CMCD data to version 1.
272
379
  *
273
- * @public
380
+ * - Extracts `nrr` from the `nor` SfItem's `r` parameter.
381
+ * - Unwraps inner-list values to scalars for the object type `ot`.
382
+ *
383
+ * @internal
274
384
  */
275
- const CMCD_REQUEST_KEYS = [
276
- "ab",
277
- "bg",
278
- "bl",
279
- "br",
280
- "bs",
281
- "bsa",
282
- "bsd",
283
- "bsda",
284
- "cdn",
285
- "cid",
286
- "cs",
287
- "d",
288
- "dfa",
289
- "dl",
290
- "ec",
291
- "lab",
292
- "lb",
293
- "ltc",
294
- "msd",
295
- "mtp",
296
- "nor",
297
- "nr",
298
- "ot",
299
- "pb",
300
- "pr",
301
- "pt",
302
- "rtp",
303
- "sf",
304
- "sid",
305
- "sn",
306
- "st",
307
- "sta",
308
- "su",
309
- "tab",
310
- "tb",
311
- "tbl",
312
- "tpb",
313
- "v"
314
- ];
385
+ function downConvertToV1(obj, ot) {
386
+ const result = {};
387
+ for (const [key, value] of Object.entries(obj)) {
388
+ if (value == null) {
389
+ result[key] = value;
390
+ continue;
391
+ }
392
+ if (key === "nor") {
393
+ const list = value instanceof SfItem && Array.isArray(value.value) ? value.value : value;
394
+ const first = Array.isArray(list) ? list[0] : list;
395
+ if (first instanceof SfItem) {
396
+ result["nor"] = first.value;
397
+ if (first.params?.r) result["nrr"] = first.params.r;
398
+ } else result["nor"] = first;
399
+ } else if (CMCD_INNER_LIST_KEYS.has(key)) result[key] = unwrapValue(value, ot);
400
+ else result[key] = value;
401
+ }
402
+ return result;
403
+ }
315
404
 
316
405
  //#endregion
317
406
  //#region src/isCmcdCustomKey.ts
318
407
  const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
319
408
  /**
320
409
  * 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.
410
+ * characters from `a-z 0-9 . -`. The key needs a hyphen, not at the start
411
+ * or end. This check applies the CTA-5004-B custom-key rules, restricted
412
+ * to names that RFC 8941 key serialization accepts.
324
413
  *
325
414
  * @param key - The key to check.
326
415
  *
@@ -337,254 +426,245 @@ function isCmcdCustomKey(key) {
337
426
  }
338
427
 
339
428
  //#endregion
340
- //#region src/isCmcdRequestKey.ts
341
- const CMCD_REQUEST_KEY_SET = /* @__PURE__ */ new Set(CMCD_REQUEST_KEYS);
429
+ //#region src/getKeySpec.ts
430
+ const CUSTOM_KEY_SPEC = {
431
+ type: "custom",
432
+ omitDefault: false,
433
+ max: 64,
434
+ v1Max: Infinity
435
+ };
342
436
  /**
343
- * Check if a key is a valid CMCD request key.
437
+ * Returns the row of a key in the key table.
344
438
  *
345
- * @param key - The key to check.
439
+ * @param key - The key to look up.
346
440
  *
347
- * @returns `true` if the key is a valid CMCD request key, `false` otherwise.
441
+ * @returns The row of a reserved key, the custom row for a valid custom key, or `undefined`.
348
442
  *
349
- * @public
350
- *
351
- * @example
352
- * {@includeCode ../test/isCmcdRequestKey.test.ts#example}
443
+ * @internal
353
444
  */
354
- function isCmcdRequestKey(key) {
355
- return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
445
+ function getKeySpec(key) {
446
+ if (Object.prototype.hasOwnProperty.call(CMCD_KEY_SPECS, key)) return CMCD_KEY_SPECS[key];
447
+ return isCmcdCustomKey(key) ? CUSTOM_KEY_SPEC : void 0;
356
448
  }
357
449
 
358
450
  //#endregion
359
- //#region src/CMCD_RESPONSE_KEYS.ts
360
- /**
361
- * CMCD v2 - Response-only and timing keys.
362
- *
363
- * @public
364
- */
365
- const CMCD_RESPONSE_KEYS = [
366
- "cmsdd",
367
- "cmsds",
368
- "rc",
369
- "smrt",
370
- "ttfb",
371
- "ttfbb",
372
- "ttlb",
373
- "url"
374
- ];
375
-
376
- //#endregion
377
- //#region src/isCmcdResponseReceivedKey.ts
378
- const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
451
+ //#region src/isValid.ts
379
452
  /**
380
- * Check if a key is a valid CMCD response key.
453
+ * Checks if the given value is valid to send as a CMCD value.
381
454
  *
382
- * @param key - The key to check.
455
+ * `undefined`, `null`, an empty string, `false`, an empty array, and a
456
+ * number that is not finite are not valid. CTA-5004-B requires omitting
457
+ * the key when its value is unknown.
383
458
  *
384
- * @returns `true` if the key is a valid CMCD request key, `false` otherwise.
459
+ * @param value - The value to check.
385
460
  *
386
- * @public
461
+ * @returns `true` if the value is valid.
387
462
  *
388
- * @example
389
- * {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
463
+ * @internal
390
464
  */
391
- function isCmcdResponseReceivedKey(key) {
392
- return CMCD_RESPONSE_KEY_SET.has(key);
465
+ function isValid(value) {
466
+ if (typeof value === "number") return Number.isFinite(value);
467
+ if (Array.isArray(value)) return value.length > 0;
468
+ return value != null && value !== "" && value !== false;
393
469
  }
394
470
 
395
471
  //#endregion
396
- //#region src/isCmcdEventKey.ts
397
- const CMCD_EVENT_KEY_SET = /* @__PURE__ */ new Set(CMCD_EVENT_KEYS);
398
- /**
399
- * Check if a key is a valid CMCD event key.
400
- *
401
- * @param key - The key to check.
402
- *
403
- * @returns `true` if the key is a valid CMCD event key, `false` otherwise.
404
- *
405
- * @public
406
- *
407
- * @example
408
- * {@includeCode ../test/isCmcdEventKey.test.ts#example}
409
- */
410
- function isCmcdEventKey(key) {
411
- return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
472
+ //#region src/formatNor.ts
473
+ function toBase(baseUrl) {
474
+ if (!baseUrl) return;
475
+ try {
476
+ return new URL(getBaseUrl(baseUrl)).href;
477
+ } catch {
478
+ return;
479
+ }
480
+ }
481
+ function formatPath(path, base, version) {
482
+ if (typeof path !== "string" || path === "") return;
483
+ const relative = base ? urlToRelativePath(path, base) : path;
484
+ return version === 1 ? encodeURIComponent(relative) : relative;
485
+ }
486
+ function formatEntry(entry, base, version) {
487
+ if (entry instanceof SfItem) {
488
+ const path = formatPath(entry.value, base, version);
489
+ return path === void 0 ? void 0 : new SfItem(path, entry.params);
490
+ }
491
+ return formatPath(entry, base, version);
492
+ }
493
+ function formatEntries(entries, base, version) {
494
+ const list = [];
495
+ for (const entry of entries) {
496
+ const formatted = formatEntry(entry, base, version);
497
+ if (formatted !== void 0) list.push(formatted);
498
+ }
499
+ return list.length > 0 ? list : void 0;
412
500
  }
413
-
414
- //#endregion
415
- //#region src/CMCD_INNER_LIST_KEYS.ts
416
- /**
417
- * Keys that are inner lists in V2 but plain scalars in V1.
418
- *
419
- * Used by both encoding (down-conversion) and decoding (up-conversion).
420
- *
421
- * @internal
422
- */
423
- const CMCD_INNER_LIST_KEYS = new Set([
424
- "ab",
425
- "bl",
426
- "br",
427
- "bsa",
428
- "bsd",
429
- "bsda",
430
- "lab",
431
- "lb",
432
- "mtp",
433
- "pb",
434
- "tab",
435
- "tb",
436
- "tbl",
437
- "tpb"
438
- ]);
439
-
440
- //#endregion
441
- //#region src/CMCD_V1_KEYS.ts
442
- /**
443
- * Defines the keys for CMCD (Common Media Client Data) version 1.
444
- *
445
- * @public
446
- */
447
- const CMCD_V1_KEYS = [
448
- "bl",
449
- "br",
450
- "bs",
451
- "cid",
452
- "d",
453
- "dl",
454
- "mtp",
455
- "nor",
456
- "nrr",
457
- "ot",
458
- "pr",
459
- "rtp",
460
- "sf",
461
- "sid",
462
- "st",
463
- "su",
464
- "tb",
465
- "v"
466
- ];
467
-
468
- //#endregion
469
- //#region src/isCmcdV1Key.ts
470
- const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
471
501
  /**
472
- * Filter function for CMCD v1 keys.
473
- *
474
- * @param key - The CMCD key to filter.
502
+ * Formats a `nor` value. Each path becomes relative to `baseUrl` when `baseUrl` is a valid URL.
503
+ * Version 1 encodes the path with `encodeURIComponent`. Version 2 wraps one path in a list.
504
+ * The parameters of an entry and of an inner list stay on the value.
475
505
  *
476
- * @returns `true` if the key should be included, `false` otherwise.
506
+ * @param value - A path, an `SfItem` of a path, or a list of them. An `SfItem` can also wrap the list.
507
+ * @param options - The version and `baseUrl` of the report.
477
508
  *
478
- * @public
509
+ * @returns The formatted value, or `undefined` when no entry is a non-empty string.
479
510
  *
480
- * @example
481
- * {@includeCode ../test/isCmcdV1Key.test.ts#example}
511
+ * @internal
482
512
  */
483
- function isCmcdV1Key(key) {
484
- return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
513
+ function formatNor(value, options) {
514
+ const base = toBase(options.baseUrl);
515
+ const version = options.version;
516
+ if (Array.isArray(value)) return formatEntries(value, base, version);
517
+ if (value instanceof SfItem && Array.isArray(value.value)) {
518
+ const list = formatEntries(value.value, base, version);
519
+ return list && new SfItem(list, value.params);
520
+ }
521
+ const entry = formatEntry(value, base, version);
522
+ return version === 1 || entry === void 0 ? entry : [entry];
485
523
  }
486
524
 
487
525
  //#endregion
488
- //#region src/isTokenField.ts
489
- const TOKEN_FIELDS = /* @__PURE__ */ new Set([
490
- "ot",
491
- "sf",
492
- "st",
493
- "e",
494
- "sta"
495
- ]);
526
+ //#region src/toTokenString.ts
496
527
  /**
497
- * Checks if the given key is a token field.
528
+ * Resolve the text of a token value.
498
529
  *
499
- * @param key - The key to check.
530
+ * A token may be a plain string, a registry `Symbol`, an `SfToken`, or an
531
+ * `SfItem` that wraps such a value.
532
+ *
533
+ * @param value - The value to resolve.
500
534
  *
501
- * @returns `true` if the key is a token field.
535
+ * @returns The token text, or `undefined` when the value is not a token.
502
536
  *
503
537
  * @internal
504
538
  */
505
- function isTokenField(key) {
506
- return TOKEN_FIELDS.has(key);
539
+ function toTokenString(value) {
540
+ if (value instanceof SfItem) return toTokenString(value.value);
541
+ if (typeof value === "string") return value;
542
+ if (typeof value === "symbol" || value instanceof SfToken) return symbolToStr(value);
507
543
  }
508
544
 
509
545
  //#endregion
510
- //#region src/isValid.ts
546
+ //#region src/normalizeValue.ts
547
+ function toText(value, spec, version) {
548
+ const max = (version === 1 ? spec.v1Max : void 0) ?? spec.max ?? Infinity;
549
+ return typeof value === "string" && value !== "" && value.length <= max ? value : void 0;
550
+ }
551
+ function normalizeBare(value, spec, version) {
552
+ switch (spec.type) {
553
+ case "boolean": return typeof value === "boolean" ? value : void 0;
554
+ case "integer":
555
+ case "ot-list": return typeof value === "number" && Number.isFinite(value) ? Math.round(value / spec.round) * spec.round : void 0;
556
+ case "decimal": return typeof value === "number" && Number.isFinite(value) ? value : void 0;
557
+ case "string": return toText(value, spec, version);
558
+ case "string-list": return typeof value === "string" && value !== "" ? value : void 0;
559
+ case "token": {
560
+ const text = toTokenString(value);
561
+ const v1Map = version === 1 ? spec.v1Map : void 0;
562
+ const token = text !== void 0 && v1Map !== void 0 && Object.prototype.hasOwnProperty.call(v1Map, text) ? v1Map[text] : text;
563
+ return token ? new SfToken(token) : void 0;
564
+ }
565
+ case "custom":
566
+ if (typeof value === "string") return toText(value, spec, version);
567
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
568
+ if (Array.isArray(value)) return value.length > 0 ? value : void 0;
569
+ return typeof value === "boolean" || typeof value === "symbol" || value instanceof SfToken ? value : void 0;
570
+ default: return;
571
+ }
572
+ }
573
+ function normalizeItem(value, spec, version) {
574
+ if (value instanceof SfItem) {
575
+ const bare = normalizeBare(value.value, spec, version);
576
+ return bare === void 0 ? void 0 : new SfItem(bare, value.params);
577
+ }
578
+ return normalizeBare(value, spec, version);
579
+ }
580
+ function normalizeList(value, spec, version) {
581
+ const wrapper = value instanceof SfItem && Array.isArray(value.value) ? value : void 0;
582
+ const items = wrapper ? wrapper.value : Array.isArray(value) ? value : [value];
583
+ const list = [];
584
+ for (const item of items) {
585
+ const normalized = normalizeItem(item, spec, version);
586
+ if (normalized !== void 0) list.push(normalized);
587
+ }
588
+ if (list.length === 0) return;
589
+ return wrapper ? new SfItem(list, wrapper.params) : list;
590
+ }
511
591
  /**
512
- * Checks if the given value is valid
592
+ * Applies the value rule of a key type from the key table.
513
593
  *
514
- * @param value - The value to check.
594
+ * An `SfItem` keeps its parameters, and the rule applies to the value inside it.
595
+ * In version 2, a list key wraps one value in a list.
596
+ * The rules do not check the limits of RFC 8941, such as the integer range.
597
+ *
598
+ * @param value - The value to prepare.
599
+ * @param spec - The row of the key in the key table.
600
+ * @param options - The version and `baseUrl` of the report.
515
601
  *
516
- * @returns `true` if the key is a value is valid.
602
+ * @returns The value to send, or `undefined` when the value fails the rule.
517
603
  *
518
604
  * @internal
519
605
  */
520
- function isValid(value) {
521
- if (typeof value === "number") return Number.isFinite(value);
522
- return value != null && value !== "" && value !== false;
606
+ function normalizeValue(value, spec, options) {
607
+ if (spec.type === "nor") return formatNor(value, options);
608
+ if (spec.type === "string-list" || spec.type === "ot-list" && options.version !== 1) return normalizeList(value, spec, options.version);
609
+ return normalizeItem(value, spec, options.version);
523
610
  }
524
611
 
525
612
  //#endregion
526
- //#region src/toTokenString.ts
613
+ //#region src/toBareValue.ts
527
614
  /**
528
- * Resolve the text of a token value.
615
+ * Returns the value inside an `SfItem`, or the value itself.
529
616
  *
530
- * A token may be a plain string, a registry `Symbol`, an `SfToken`, or an
531
- * `SfItem` that wraps one of those.
617
+ * `decodeCmcd` returns a member with parameters as an `SfItem`. For an
618
+ * inner list with parameters, the value of the `SfItem` is the list.
532
619
  *
533
- * @param value - The value to resolve.
534
- *
535
- * @returns The token text, or `undefined` when the value is not a token.
620
+ * @param value - The value to read.
621
+ * @returns The value without its parameters.
536
622
  *
537
623
  * @internal
538
624
  */
539
- function toTokenString(value) {
540
- if (value instanceof SfItem) return toTokenString(value.value);
541
- if (typeof value === "string") return value;
542
- if (typeof value === "symbol" || value instanceof SfToken) return symbolToStr(value);
625
+ function toBareValue(value) {
626
+ return value instanceof SfItem ? value.value : value;
543
627
  }
544
628
 
545
629
  //#endregion
546
630
  //#region src/prepareCmcdData.ts
547
- const filterMap = {
548
- [CMCD_EVENT_MODE]: isCmcdEventKey,
549
- [CMCD_REQUEST_MODE]: isCmcdRequestKey
550
- };
551
- /**
552
- * Unwrap an inner list or SfItem value to a plain scalar.
553
- */
554
- function unwrapValue(value, ot) {
555
- if (Array.isArray(value)) {
556
- let item;
557
- const otText = toTokenString(ot);
558
- if (otText) item = value.find((item$1) => toTokenString(item$1.params?.ot) === otText);
559
- if (!item) item = value[0];
560
- return unwrapValue(item);
561
- }
562
- if (value instanceof SfItem) return value.value;
563
- return value;
631
+ function hasKey(spec, reportingMode, isV1) {
632
+ return (spec.mode === void 0 || spec.mode === reportingMode) && (spec.version === void 0 || spec.version === 1 === isV1);
564
633
  }
565
- /**
566
- * Down-convert V2 CMCD data to V1 format.
567
- *
568
- * - Extracts `nrr` from `nor` SfItem `r` parameter.
569
- * - Unwraps inner-list values to plain scalars.
570
- */
571
- function downConvertToV1(obj) {
572
- const result = {};
573
- for (const [key, value] of Object.entries(obj)) {
574
- if (value == null) {
575
- result[key] = value;
576
- continue;
577
- }
578
- if (key === "nor") {
579
- const first = (Array.isArray(value) ? value : [value])[0];
580
- if (first instanceof SfItem) {
581
- result["nor"] = first.value;
582
- if (first.params?.r) result["nrr"] = first.params.r;
583
- } else result["nor"] = first;
584
- } else if (CMCD_INNER_LIST_KEYS.has(key)) result[key] = unwrapValue(value, obj["ot"]);
585
- else result[key] = value;
634
+ function isRequired(spec, event) {
635
+ return spec.requiredOn === "always" || event !== void 0 && spec.requiredOn === event;
636
+ }
637
+ function hasObjectTypes(spec) {
638
+ return (spec.type === "integer" || spec.type === "ot-list") && spec.ot !== void 0;
639
+ }
640
+ function needsV1ObjectType(obj, filter) {
641
+ if (typeof filter !== "function" || filter("ot")) return true;
642
+ for (const key of CMCD_INNER_LIST_KEYS) if (Array.isArray(toBareValue(obj[key])) && filter(key)) return true;
643
+ return false;
644
+ }
645
+ function allowsObjectType(spec, objectType) {
646
+ if (objectType === void 0 || spec.type !== "integer" && spec.type !== "ot-list" || spec.ot === void 0) return true;
647
+ return spec.ot.includes(objectType);
648
+ }
649
+ function isDefaultValue(spec, value) {
650
+ return "omitDefault" in spec && toBareValue(value) === spec.omitDefault;
651
+ }
652
+ function insertKey(keys, specs, key) {
653
+ let index = 0;
654
+ while (index < keys.length && keys[index] < key) index++;
655
+ keys.splice(index, 0, key);
656
+ specs.splice(index, 0, CMCD_KEY_SPECS[key]);
657
+ }
658
+ function prepareValue(key, value, spec, options, formatterOptions) {
659
+ const formatter = options.formatters?.[key];
660
+ if (typeof formatter !== "function") return value == null ? void 0 : normalizeValue(value, spec, formatterOptions);
661
+ const formatted = isValid(value) ? formatter(value, formatterOptions) : value;
662
+ if (!isValid(formatted) && !(formatted === false && spec.type === "boolean")) return;
663
+ if (spec.type === "token") {
664
+ if (typeof formatted === "string") return new SfToken(formatted);
665
+ if (formatted instanceof SfItem && typeof formatted.value === "string") return new SfItem(new SfToken(formatted.value), formatted.params);
586
666
  }
587
- return result;
667
+ return formatted;
588
668
  }
589
669
  /**
590
670
  * Convert a generic object to CMCD data.
@@ -600,48 +680,66 @@ function downConvertToV1(obj) {
600
680
  function prepareCmcdData(obj, options = {}) {
601
681
  const results = {};
602
682
  if (obj == null || typeof obj !== "object") return results;
603
- const version = options.version || obj["v"] || CMCD_V2;
683
+ const version = options.version || toBareValue(obj["v"]) || CMCD_V2;
604
684
  const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
605
- const data = version === 1 ? downConvertToV1(obj) : obj;
606
- const eventType = toTokenString(data["e"]);
607
- const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode];
608
- let keys = Object.keys(data).filter(keyFilter);
609
- if (data["e"] && eventType !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
610
- const filter = options.filter;
611
- if (typeof filter === "function") keys = keys.filter(filter);
612
- const isEventMode = reportingMode === CMCD_EVENT_MODE;
613
- if (isEventMode) {
614
- if (!keys.includes("e") && data["e"] != null) keys.push("e");
615
- if (!keys.includes("ts")) keys.push("ts");
616
- if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
617
- if (!keys.includes("ec") && data["ec"] != null && eventType === CMCD_EVENT_ERROR) keys.push("ec");
618
- if (!keys.includes("url") && data["url"] != null && eventType === CMCD_EVENT_RESPONSE_RECEIVED) keys.push("url");
619
- const requiredField = eventType ? CMCD_STATE_EVENT_FIELDS.get(eventType) : void 0;
620
- if (requiredField && data[requiredField] != null && !keys.includes(requiredField)) keys.push(requiredField);
621
- }
622
- if (keys.length === 0) return results;
623
- if (version > 1 && !keys.includes("v")) keys.push("v");
685
+ const isV1 = version === 1;
686
+ const isEventReport = !isV1 && reportingMode === CMCD_EVENT_MODE;
624
687
  const formatterOptions = {
625
688
  version,
626
689
  reportingMode,
627
690
  baseUrl: options.baseUrl
628
691
  };
629
- keys.sort();
630
- for (const key of keys) {
631
- let value = data[key];
632
- const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
633
- if (typeof formatter === "function") value = formatter(value, formatterOptions);
634
- if (key === "v") if (version === 1) continue;
635
- else value = version;
636
- if (key === "pr" && value === 1 && !(isEventMode && eventType === CMCD_EVENT_PLAYBACK_RATE)) continue;
637
- if (isEventMode && key === "ts" && !Number.isFinite(value)) value = Date.now();
638
- const isBgFalseTransition = isEventMode && value === false && key === "bg" && eventType === CMCD_EVENT_BACKGROUNDED_MODE;
639
- if (!isValid(value) && !isBgFalseTransition) continue;
640
- if (isTokenField(key)) {
641
- if (typeof value === "string") value = new SfToken(value);
642
- else if (value instanceof SfItem && typeof value.value === "string") value = new SfItem(new SfToken(value.value), value.params);
692
+ const filter = options.filter;
693
+ let objectTypeValue;
694
+ let objectType;
695
+ if (isV1 && needsV1ObjectType(obj, filter)) {
696
+ objectTypeValue = prepareValue("ot", obj["ot"], CMCD_KEY_SPECS["ot"], options, formatterOptions);
697
+ objectType = toTokenString(objectTypeValue);
698
+ }
699
+ const data = isV1 ? downConvertToV1(obj, objectType) : obj;
700
+ const eventValue = isEventReport ? prepareValue("e", data["e"], CMCD_KEY_SPECS["e"], options, formatterOptions) : void 0;
701
+ const event = toTokenString(eventValue);
702
+ const keys = [];
703
+ const specs = [];
704
+ let passed = false;
705
+ let needsObjectType = false;
706
+ for (const key of Object.keys(data).sort()) {
707
+ const spec = getKeySpec(key);
708
+ if (spec === void 0 || !hasKey(spec, reportingMode, isV1)) continue;
709
+ if (typeof filter !== "function" || filter(key)) passed = true;
710
+ else if (!isRequired(spec, event)) continue;
711
+ if (spec.onlyOn !== void 0 && spec.onlyOn !== event) continue;
712
+ if (key === "ot" || hasObjectTypes(spec)) needsObjectType = true;
713
+ keys.push(key);
714
+ specs.push(spec);
715
+ }
716
+ if (!passed && !isEventReport) return results;
717
+ if (needsObjectType && !isV1) {
718
+ objectTypeValue = prepareValue("ot", data["ot"], CMCD_KEY_SPECS["ot"], options, formatterOptions);
719
+ objectType = toTokenString(objectTypeValue);
720
+ }
721
+ if (isEventReport && !keys.includes("ts")) insertKey(keys, specs, "ts");
722
+ if (!isV1 && !keys.includes("v")) insertKey(keys, specs, "v");
723
+ const values = new Array(keys.length);
724
+ for (let i = keys.length - 1; i >= 0; i--) {
725
+ const key = keys[i];
726
+ const spec = specs[i];
727
+ if (spec.type === "ot-list" && spec.supersededBy !== void 0) {
728
+ const exact = keys.indexOf(spec.supersededBy, i + 1);
729
+ if (exact !== -1 && values[exact] !== void 0) continue;
730
+ }
731
+ if (key === "v") values[i] = version;
732
+ else if (key === "ot") values[i] = objectTypeValue;
733
+ else if (key === "e") values[i] = eventValue;
734
+ else if (isV1 || allowsObjectType(spec, objectType)) values[i] = prepareValue(key, data[key], spec, options, formatterOptions);
735
+ if (key === "ts") {
736
+ const timestamp = toBareValue(values[i]);
737
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) values[i] = Date.now();
643
738
  }
644
- results[key] = value;
739
+ }
740
+ for (let i = 0; i < keys.length; i++) {
741
+ const value = values[i];
742
+ if (value !== void 0 && (!isDefaultValue(specs[i], value) || isRequired(specs[i], event))) results[keys[i]] = value;
645
743
  }
646
744
  return results;
647
745
  }
@@ -703,9 +801,9 @@ const CMCD_HEADER_FIELDS = [
703
801
  /**
704
802
  * The map of CMCD keys to their appropriate header shard.
705
803
  *
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.
804
+ * Event-only keys (e, ts, cen, h) and response-received keys (rc, ttfb,
805
+ * ttlb, url, and others) are intentionally absent. They are transmitted
806
+ * in the event-mode POST body, not HTTP headers.
709
807
  *
710
808
  * @public
711
809
  */
@@ -760,9 +858,9 @@ function createHeaderMap(headerMap) {
760
858
  }, {});
761
859
  }
762
860
  /**
763
- * Group a CMCD data object into header shards
861
+ * Group a CMCD object into header shards
764
862
  *
765
- * @param cmcd - The CMCD data object to convert.
863
+ * @param cmcd - The CMCD object to convert.
766
864
  * @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
767
865
  *
768
866
  * @returns The CMCD header shards.
@@ -808,10 +906,10 @@ function toPreparedCmcdHeaders(data, customHeaderMap) {
808
906
  //#endregion
809
907
  //#region src/toCmcdHeaders.ts
810
908
  /**
811
- * Convert a CMCD data object to request headers
909
+ * Convert a CMCD object to request headers
812
910
  *
813
- * @param cmcd - The CMCD data object to convert.
814
- * @param options - Options for encoding the CMCD object.
911
+ * @param cmcd - The CMCD object to convert.
912
+ * @param options - Options for encoding.
815
913
  *
816
914
  * @returns The CMCD header shards.
817
915
  *
@@ -830,13 +928,13 @@ function toCmcdHeaders(cmcd, options = {}) {
830
928
  //#endregion
831
929
  //#region src/appendCmcdHeaders.ts
832
930
  /**
833
- * Append CMCD query args to a header object.
931
+ * Append CMCD header shards to a header object.
834
932
  *
835
933
  * @param headers - The headers to append to.
836
934
  * @param cmcd - The CMCD object to append.
837
- * @param options - Encode options.
935
+ * @param options - Options for encoding.
838
936
  *
839
- * @returns The headers with the CMCD header shards appended.
937
+ * @returns The headers with CMCD header shards appended.
840
938
  *
841
939
  * @public
842
940
  *
@@ -897,10 +995,10 @@ function encodeCmcd(cmcd, options = {}) {
897
995
  //#endregion
898
996
  //#region src/toCmcdUrl.ts
899
997
  /**
900
- * Convert a CMCD data object to a URL encoded string.
998
+ * Convert a CMCD object to a URL encoded string.
901
999
  *
902
1000
  * @param cmcd - The CMCD object to convert.
903
- * @param options - Options for encoding the CMCD object.
1001
+ * @param options - Options for encoding.
904
1002
  *
905
1003
  * @returns The URL encoded CMCD data.
906
1004
  *
@@ -917,10 +1015,10 @@ function toCmcdUrl(cmcd, options = {}) {
917
1015
  //#endregion
918
1016
  //#region src/toCmcdQuery.ts
919
1017
  /**
920
- * Convert a CMCD data object to a query arg.
1018
+ * Convert a CMCD object to a query arg.
921
1019
  *
922
1020
  * @param cmcd - The CMCD object to convert.
923
- * @param options - Options for encoding the CMCD object.
1021
+ * @param options - Options for encoding.
924
1022
  *
925
1023
  * @returns The CMCD query arg.
926
1024
  *
@@ -944,91 +1042,402 @@ const REGEX = /CMCD=[^&#]+/;
944
1042
  *
945
1043
  * @param url - The URL to append to.
946
1044
  * @param cmcd - The CMCD object to append.
947
- * @param options - Options for encoding the CMCD object.
1045
+ * @param options - Options for encoding.
948
1046
  *
949
1047
  * @returns The URL with the CMCD query args appended.
950
1048
  *
951
1049
  * @public
952
- *
953
- * @example
954
- * {@includeCode ../test/appendCmcdQuery.test.ts#example}
955
- *
956
- * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-B Query Argument Definition}
1050
+ *
1051
+ * @example
1052
+ * {@includeCode ../test/appendCmcdQuery.test.ts#example}
1053
+ *
1054
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-B Query Argument Definition}
1055
+ */
1056
+ function appendCmcdQuery(url, cmcd, options) {
1057
+ const query = toCmcdQuery(cmcd, options);
1058
+ if (!query) return url;
1059
+ if (REGEX.test(url)) return url.replace(REGEX, query);
1060
+ return `${url}${url.includes("?") ? "&" : "?"}${query}`;
1061
+ }
1062
+
1063
+ //#endregion
1064
+ //#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
1065
+ /**
1066
+ * The default time interval in seconds when using event mode
1067
+ *
1068
+ * @public
1069
+ */
1070
+ const CMCD_DEFAULT_TIME_INTERVAL = 30;
1071
+
1072
+ //#endregion
1073
+ //#region src/CMCD_EVENT_KEYS.ts
1074
+ /**
1075
+ * Defines the event-specific keys for CMCD (Common Media Client Data) version 2.
1076
+ *
1077
+ * @public
1078
+ */
1079
+ const CMCD_EVENT_KEYS = [
1080
+ "cen",
1081
+ "e",
1082
+ "h",
1083
+ "ts"
1084
+ ];
1085
+
1086
+ //#endregion
1087
+ //#region src/CMCD_FORMATTER_MAP.ts
1088
+ const formatNumbers = (value, format) => {
1089
+ if (Array.isArray(value)) {
1090
+ const list = [];
1091
+ for (const item of value) {
1092
+ const formatted = formatNumbers(item, format);
1093
+ if (Number.isFinite(formatted instanceof SfItem ? formatted.value : formatted)) list.push(formatted);
1094
+ }
1095
+ return list;
1096
+ }
1097
+ if (value instanceof SfItem) {
1098
+ const formatted = formatNumbers(value.value, format);
1099
+ return isValid(formatted) ? new SfItem(formatted, value.params) : NaN;
1100
+ }
1101
+ return typeof value === "number" ? format(value) : NaN;
1102
+ };
1103
+ const toRounded = (value) => formatNumbers(value, Math.round);
1104
+ const toNumber = (value) => formatNumbers(value, Number);
1105
+ const roundToHundred = (value) => Math.round(value / 100) * 100;
1106
+ const toHundred = (value) => formatNumbers(value, roundToHundred);
1107
+ /**
1108
+ * Formatters for CMCD values.
1109
+ *
1110
+ * The encoder applies the same rounding through its key table, and `nor` is the `nor` rule of the encoder.
1111
+ * Use an entry to build a custom formatter for `CmcdEncodeOptions.formatters`.
1112
+ * The formatter of a numeric key drops a value or a list element that is not a finite number.
1113
+ *
1114
+ * @public
1115
+ */
1116
+ const CMCD_FORMATTER_MAP = {
1117
+ br: toRounded,
1118
+ d: toRounded,
1119
+ bl: toHundred,
1120
+ dl: toHundred,
1121
+ mtp: toHundred,
1122
+ nor: (value, options) => formatNor(value, options) ?? "",
1123
+ rtp: toHundred,
1124
+ tb: toRounded,
1125
+ tbl: toHundred,
1126
+ ab: toRounded,
1127
+ bsa: toRounded,
1128
+ bsd: toRounded,
1129
+ bsda: toRounded,
1130
+ dfa: toRounded,
1131
+ lab: toRounded,
1132
+ lb: toRounded,
1133
+ ltc: toRounded,
1134
+ msd: toRounded,
1135
+ pb: toRounded,
1136
+ pr: toNumber,
1137
+ pt: toRounded,
1138
+ rc: toRounded,
1139
+ sn: toRounded,
1140
+ tab: toRounded,
1141
+ tpb: toRounded,
1142
+ ts: toRounded,
1143
+ ttfb: toRounded,
1144
+ ttfbb: toRounded,
1145
+ ttlb: toRounded
1146
+ };
1147
+
1148
+ //#endregion
1149
+ //#region src/CMCD_REQUEST_KEYS.ts
1150
+ /**
1151
+ * Defines the request-specific keys for CMCD (Common Media Client Data) version 2.
1152
+ *
1153
+ * @public
1154
+ */
1155
+ const CMCD_REQUEST_KEYS = [
1156
+ "ab",
1157
+ "bg",
1158
+ "bl",
1159
+ "br",
1160
+ "bs",
1161
+ "bsa",
1162
+ "bsd",
1163
+ "bsda",
1164
+ "cdn",
1165
+ "cid",
1166
+ "cs",
1167
+ "d",
1168
+ "dfa",
1169
+ "dl",
1170
+ "ec",
1171
+ "lab",
1172
+ "lb",
1173
+ "ltc",
1174
+ "msd",
1175
+ "mtp",
1176
+ "nor",
1177
+ "nr",
1178
+ "ot",
1179
+ "pb",
1180
+ "pr",
1181
+ "pt",
1182
+ "rtp",
1183
+ "sf",
1184
+ "sid",
1185
+ "sn",
1186
+ "st",
1187
+ "sta",
1188
+ "su",
1189
+ "tab",
1190
+ "tb",
1191
+ "tbl",
1192
+ "tpb",
1193
+ "v"
1194
+ ];
1195
+
1196
+ //#endregion
1197
+ //#region src/CMCD_RESPONSE_KEYS.ts
1198
+ /**
1199
+ * Response-only and timing keys for version 2.
1200
+ *
1201
+ * @public
1202
+ */
1203
+ const CMCD_RESPONSE_KEYS = [
1204
+ "cmsdd",
1205
+ "cmsds",
1206
+ "rc",
1207
+ "smrt",
1208
+ "ttfb",
1209
+ "ttfbb",
1210
+ "ttlb",
1211
+ "url"
1212
+ ];
1213
+
1214
+ //#endregion
1215
+ //#region src/CMCD_V1_KEYS.ts
1216
+ /**
1217
+ * Defines the keys for CMCD (Common Media Client Data) version 1.
1218
+ *
1219
+ * @public
1220
+ */
1221
+ const CMCD_V1_KEYS = [
1222
+ "bl",
1223
+ "br",
1224
+ "bs",
1225
+ "cid",
1226
+ "d",
1227
+ "dl",
1228
+ "mtp",
1229
+ "nor",
1230
+ "nrr",
1231
+ "ot",
1232
+ "pr",
1233
+ "rtp",
1234
+ "sf",
1235
+ "sid",
1236
+ "st",
1237
+ "su",
1238
+ "tb",
1239
+ "v"
1240
+ ];
1241
+
1242
+ //#endregion
1243
+ //#region src/CMCD_KEYS.ts
1244
+ /**
1245
+ * A list of all CMCD keys.
1246
+ *
1247
+ * @public
1248
+ */
1249
+ const CMCD_KEYS = /* @__PURE__ */ [
1250
+ ...CMCD_V1_KEYS,
1251
+ ...CMCD_REQUEST_KEYS,
1252
+ ...CMCD_RESPONSE_KEYS,
1253
+ ...CMCD_EVENT_KEYS
1254
+ ].filter((key, index, arr) => arr.indexOf(key) === index);
1255
+
1256
+ //#endregion
1257
+ //#region src/CMCD_MIME_TYPE.ts
1258
+ /**
1259
+ * CMCD MIME type for event report payloads.
1260
+ *
1261
+ * @public
1262
+ */
1263
+ const CMCD_MIME_TYPE = "application/cmcd";
1264
+
1265
+ //#endregion
1266
+ //#region src/CMCD_REQUEST_PROVENANCE.ts
1267
+ /**
1268
+ * The `customData` property where `CmcdReporter.createRequestReport()`
1269
+ * writes session provenance on every request it returns.
1270
+ *
1271
+ * The value is a frozen `CmcdRequestProvenance` record. `sid` names the
1272
+ * session that issued the request. `cid` is the content id at the time the
1273
+ * request was issued. `data` is the request's per-call data as an encoded
1274
+ * CMCD string. `CmcdReporter.recordResponseReceived()` reads the record to
1275
+ * attribute a late response to its session and to rebuild the request-time
1276
+ * report keys.
1277
+ * Spread and `Object.assign` copy the record into ordinary request clones.
1278
+ * `JSON.stringify` and structured clone drop symbol-keyed properties. Before
1279
+ * either operation, read the value and restore it afterward. JSON preserves
1280
+ * the record's fields. Attribution uses the `sid` value rather than object
1281
+ * identity, so a parsed plain-object copy attributes correctly. For a record
1282
+ * naming a `sid` the reporter does not retain, the reporter drops the
1283
+ * response rather than reattributing it.
1284
+ *
1285
+ * The symbol registry provides the symbol, so duplicated copies of this
1286
+ * library in one bundle interoperate. The registry key is stable across
1287
+ * versions.
1288
+ *
1289
+ * @public
1290
+ */
1291
+ const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
1292
+
1293
+ //#endregion
1294
+ //#region src/CMCD_V1.ts
1295
+ /**
1296
+ * CMCD Version 1
1297
+ *
1298
+ * @public
1299
+ */
1300
+ const CMCD_V1 = 1;
1301
+
1302
+ //#endregion
1303
+ //#region src/CmcdEventType.ts
1304
+ /**
1305
+ * CMCD event type 'bc' (bitrate change).
1306
+ *
1307
+ * @public
1308
+ */
1309
+ const CMCD_EVENT_BITRATE_CHANGE = "bc";
1310
+ /**
1311
+ * CMCD event type 'ps' (play state change).
1312
+ *
1313
+ * @public
1314
+ */
1315
+ const CMCD_EVENT_PLAY_STATE = "ps";
1316
+ /**
1317
+ * CMCD event type 'pr' (playback rate change).
1318
+ *
1319
+ * @public
1320
+ */
1321
+ const CMCD_EVENT_PLAYBACK_RATE = "pr";
1322
+ /**
1323
+ * CMCD event type 'e' (error).
1324
+ *
1325
+ * @public
1326
+ */
1327
+ const CMCD_EVENT_ERROR = "e";
1328
+ /**
1329
+ * CMCD event type 't' (time interval).
1330
+ *
1331
+ * @public
1332
+ */
1333
+ const CMCD_EVENT_TIME_INTERVAL = "t";
1334
+ /**
1335
+ * CMCD event type 'c' (content ID).
1336
+ *
1337
+ * @public
1338
+ */
1339
+ const CMCD_EVENT_CONTENT_ID = "c";
1340
+ /**
1341
+ * CMCD event type 'b' (backgrounded mode).
1342
+ *
1343
+ * @public
1344
+ */
1345
+ const CMCD_EVENT_BACKGROUNDED_MODE = "b";
1346
+ /**
1347
+ * CMCD event type 'm' (mute).
1348
+ *
1349
+ * @public
1350
+ */
1351
+ const CMCD_EVENT_MUTE = "m";
1352
+ /**
1353
+ * CMCD event type 'um' (unmute).
1354
+ *
1355
+ * @public
1356
+ */
1357
+ const CMCD_EVENT_UNMUTE = "um";
1358
+ /**
1359
+ * CMCD event type 'pe' (player expand).
1360
+ *
1361
+ * @public
1362
+ */
1363
+ const CMCD_EVENT_PLAYER_EXPAND = "pe";
1364
+ /**
1365
+ * CMCD event type 'pc' (player collapse).
1366
+ *
1367
+ * @public
1368
+ */
1369
+ const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
1370
+ /**
1371
+ * CMCD event type 'rr' (response received).
1372
+ *
1373
+ * @public
957
1374
  */
958
- function appendCmcdQuery(url, cmcd, options) {
959
- const query = toCmcdQuery(cmcd, options);
960
- if (!query) return url;
961
- if (REGEX.test(url)) return url.replace(REGEX, query);
962
- return `${url}${url.includes("?") ? "&" : "?"}${query}`;
963
- }
964
-
965
- //#endregion
966
- //#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
1375
+ const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
967
1376
  /**
968
- * The default time interval in seconds when using using event mode
1377
+ * CMCD event type 'as' (ad start).
969
1378
  *
970
1379
  * @public
971
1380
  */
972
- const CMCD_DEFAULT_TIME_INTERVAL = 30;
973
-
974
- //#endregion
975
- //#region src/CMCD_KEYS.ts
1381
+ const CMCD_EVENT_AD_START = "as";
976
1382
  /**
977
- * A list of all CMCD keys.
1383
+ * CMCD event type 'ae' (ad end).
978
1384
  *
979
1385
  * @public
980
1386
  */
981
- const CMCD_KEYS = /* @__PURE__ */ [
982
- ...CMCD_V1_KEYS,
983
- ...CMCD_REQUEST_KEYS,
984
- ...CMCD_RESPONSE_KEYS,
985
- ...CMCD_EVENT_KEYS
986
- ].filter((key, index, arr) => arr.indexOf(key) === index);
987
-
988
- //#endregion
989
- //#region src/CMCD_MIME_TYPE.ts
1387
+ const CMCD_EVENT_AD_END = "ae";
990
1388
  /**
991
- * CMCD MIME type for event report payloads.
1389
+ * CMCD event type 'abs' (ad break start).
992
1390
  *
993
1391
  * @public
994
1392
  */
995
- const CMCD_MIME_TYPE = "application/cmcd";
996
-
997
- //#endregion
998
- //#region src/CMCD_REQUEST_PROVENANCE.ts
1393
+ const CMCD_EVENT_AD_BREAK_START = "abs";
999
1394
  /**
1000
- * The `customData` key under which `CmcdReporter.createRequestReport()`
1001
- * stamps session provenance on every request it returns.
1395
+ * CMCD event type 'abe' (ad break end).
1002
1396
  *
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.
1397
+ * @public
1398
+ */
1399
+ const CMCD_EVENT_AD_BREAK_END = "abe";
1400
+ /**
1401
+ * CMCD event type 'sk' (skip).
1016
1402
  *
1017
- * Backed by the symbol registry so duplicated copies of this library in
1018
- * one bundle interoperate. The registry key is stable across versions.
1403
+ * @public
1404
+ */
1405
+ const CMCD_EVENT_SKIP = "sk";
1406
+ /**
1407
+ * CMCD event type 'ce' (custom event).
1019
1408
  *
1020
1409
  * @public
1021
1410
  */
1022
- const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
1023
-
1024
- //#endregion
1025
- //#region src/CMCD_V1.ts
1411
+ const CMCD_EVENT_CUSTOM_EVENT = "ce";
1026
1412
  /**
1027
- * CMCD Version 1
1413
+ * CMCD event types for the 'e' key (event mode).
1414
+ *
1415
+ * @enum
1416
+ *
1417
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-B Event}
1028
1418
  *
1029
1419
  * @public
1030
1420
  */
1031
- const CMCD_V1 = 1;
1421
+ const CmcdEventType = {
1422
+ BITRATE_CHANGE: CMCD_EVENT_BITRATE_CHANGE,
1423
+ PLAY_STATE: CMCD_EVENT_PLAY_STATE,
1424
+ PLAYBACK_RATE: CMCD_EVENT_PLAYBACK_RATE,
1425
+ ERROR: CMCD_EVENT_ERROR,
1426
+ TIME_INTERVAL: CMCD_EVENT_TIME_INTERVAL,
1427
+ CONTENT_ID: CMCD_EVENT_CONTENT_ID,
1428
+ BACKGROUNDED_MODE: CMCD_EVENT_BACKGROUNDED_MODE,
1429
+ MUTE: CMCD_EVENT_MUTE,
1430
+ UNMUTE: CMCD_EVENT_UNMUTE,
1431
+ PLAYER_EXPAND: CMCD_EVENT_PLAYER_EXPAND,
1432
+ PLAYER_COLLAPSE: CMCD_EVENT_PLAYER_COLLAPSE,
1433
+ RESPONSE_RECEIVED: CMCD_EVENT_RESPONSE_RECEIVED,
1434
+ AD_START: CMCD_EVENT_AD_START,
1435
+ AD_END: CMCD_EVENT_AD_END,
1436
+ AD_BREAK_START: CMCD_EVENT_AD_BREAK_START,
1437
+ AD_BREAK_END: CMCD_EVENT_AD_BREAK_END,
1438
+ SKIP: CMCD_EVENT_SKIP,
1439
+ CUSTOM_EVENT: CMCD_EVENT_CUSTOM_EVENT
1440
+ };
1032
1441
 
1033
1442
  //#endregion
1034
1443
  //#region src/CmcdObjectType.ts
@@ -1056,7 +1465,7 @@ const CmcdObjectType = {
1056
1465
  //#endregion
1057
1466
  //#region src/CmcdPlayerState.ts
1058
1467
  /**
1059
- * CMCD v2 player states for the 'sta' key.
1468
+ * CMCD version 2 player states for the 'sta' key.
1060
1469
  *
1061
1470
  *
1062
1471
  * @enum
@@ -1081,29 +1490,29 @@ const CmcdPlayerState = {
1081
1490
  //#endregion
1082
1491
  //#region src/CmcdRecordedReportMode.ts
1083
1492
  /**
1084
- * CMCD query reporting mode — CMCD data carried as a `CMCD=` URL
1493
+ * CMCD query reporting mode: CMCD data transmitted as a `CMCD=` URL
1085
1494
  * query parameter.
1086
1495
  *
1087
1496
  * @public
1088
1497
  */
1089
1498
  const CMCD_RECORDED_REPORT_MODE_QUERY = "query";
1090
1499
  /**
1091
- * CMCD header reporting mode — CMCD data carried as `Cmcd-*` HTTP
1500
+ * CMCD header reporting mode: CMCD data transmitted as `CMCD-*` HTTP
1092
1501
  * headers.
1093
1502
  *
1094
1503
  * @public
1095
1504
  */
1096
1505
  const CMCD_RECORDED_REPORT_MODE_HEADER = "header";
1097
1506
  /**
1098
- * CMCD event reporting mode — CMCD data carried as a POST body to an
1507
+ * CMCD event reporting mode: CMCD data transmitted as a POST body to an
1099
1508
  * event-target URL.
1100
1509
  *
1101
1510
  * @public
1102
1511
  */
1103
1512
  const CMCD_RECORDED_REPORT_MODE_EVENT = "event";
1104
1513
  /**
1105
- * Reporting mode under which a captured report was observed by
1106
- * `CmcdReportRecorder`.
1514
+ * Reporting mode in which `CmcdReportRecorder` observed a captured
1515
+ * report.
1107
1516
  *
1108
1517
  * @enum
1109
1518
  *
@@ -1137,7 +1546,7 @@ const CMCD_RECORDED_REQUEST_TYPE_SEGMENT = "segment";
1137
1546
  */
1138
1547
  const CMCD_RECORDED_REQUEST_TYPE_EVENT = "event";
1139
1548
  /**
1140
- * Fallback classification for requests the recorder did not recognize
1549
+ * Default classification for requests the recorder did not recognize
1141
1550
  * as manifest, segment, or event.
1142
1551
  *
1143
1552
  * @public
@@ -1179,17 +1588,17 @@ async function toHttpRequest(request) {
1179
1588
  };
1180
1589
  }
1181
1590
  /**
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
1591
+ * Create a transport adapter for `CmcdReportRecorder` that patches
1592
+ * `globalThis.fetch` to capture requests with CMCD data. The patched
1593
+ * `fetch` normalizes each request to `HttpRequest` (lowercase headers,
1594
+ * body read as a UTF-8 string).
1595
+ *
1596
+ * The patched `fetch` reads each body once with `Request.text()` for
1597
+ * inspection. This read is safe for the body types that CMCD reports use
1598
+ * in practice: `string`, `Blob`, `ArrayBuffer`, `FormData`, and
1599
+ * `URLSearchParams`. The underlying `fetch` can read those bodies again.
1600
+ * Do not pass a `ReadableStream` as `init.body`. The patched `fetch`
1601
+ * consumes the stream, and the underlying `fetch` receives an
1193
1602
  * already-disturbed stream.
1194
1603
  *
1195
1604
  * @public
@@ -1255,9 +1664,8 @@ async function completeXhrWith(xhr, response) {
1255
1664
  });
1256
1665
  }
1257
1666
  /**
1258
- * Create a transport adapter that patches `XMLHttpRequest.prototype` to
1259
- * capture CMCD-bearing requests. Returns the adapter object expected by
1260
- * `CmcdReportRecorder`.
1667
+ * Create a transport adapter for `CmcdReportRecorder` that patches
1668
+ * `XMLHttpRequest.prototype` to capture requests with CMCD data.
1261
1669
  *
1262
1670
  * @public
1263
1671
  */
@@ -1320,10 +1728,10 @@ function detectReportingMode(url, headers, isEventTarget) {
1320
1728
  return url.includes(`${CMCD_PARAM}=`) ? "query" : null;
1321
1729
  }
1322
1730
  /**
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.
1731
+ * Test helper that records CMCD reports from the XHR and fetch
1732
+ * transports for assertions in e2e tests. The recorder normalizes each
1733
+ * captured request to {@link @svta/cml-utils!HttpRequest | HttpRequest},
1734
+ * so tests are identical regardless of which transport the player uses.
1327
1735
  *
1328
1736
  * @example
1329
1737
  * {@includeCode ../test/CmcdReportRecorder.test.ts#example}
@@ -1391,11 +1799,11 @@ var CmcdReportRecorder = class {
1391
1799
  });
1392
1800
  }
1393
1801
  /**
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.
1802
+ * Install transport patches and start recording CMCD reports.
1803
+ * If the recorder is already attached, this call does nothing. The
1804
+ * recorder keeps the previous options and silently ignores the new
1805
+ * `options` argument. To re-attach with different options, call
1806
+ * `detach()` first.
1399
1807
  *
1400
1808
  * @public
1401
1809
  */
@@ -1436,7 +1844,7 @@ var CmcdReportRecorder = class {
1436
1844
  this.#reports = [];
1437
1845
  }
1438
1846
  /**
1439
- * Return a defensive copy of all recorded reports.
1847
+ * Return a copy of all recorded reports.
1440
1848
  *
1441
1849
  * @public
1442
1850
  */
@@ -1445,8 +1853,8 @@ var CmcdReportRecorder = class {
1445
1853
  }
1446
1854
  /**
1447
1855
  * 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
1856
+ * Resolves with all matching reports. Rejects with a diagnostic error
1857
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1450
1858
  * recorder's `waitTimeout` option (default 15000 ms).
1451
1859
  *
1452
1860
  * @public
@@ -1456,8 +1864,8 @@ var CmcdReportRecorder = class {
1456
1864
  }
1457
1865
  /**
1458
1866
  * 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
1867
+ * Resolves with all matching reports. Rejects with a diagnostic error
1868
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1461
1869
  * recorder's `waitTimeout` option (default 15000 ms).
1462
1870
  *
1463
1871
  * @public
@@ -1467,8 +1875,8 @@ var CmcdReportRecorder = class {
1467
1875
  }
1468
1876
  /**
1469
1877
  * 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
1878
+ * Resolves with all matching reports. Rejects with a diagnostic error
1879
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1472
1880
  * recorder's `waitTimeout` option (default 15000 ms).
1473
1881
  *
1474
1882
  * @public
@@ -1478,8 +1886,8 @@ var CmcdReportRecorder = class {
1478
1886
  }
1479
1887
  /**
1480
1888
  * 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
1889
+ * Resolves with all matching reports. Rejects with a diagnostic error
1890
+ * on timeout. `count` defaults to 1. `timeout` defaults to the
1483
1891
  * recorder's `waitTimeout` option (default 15000 ms).
1484
1892
  *
1485
1893
  * @public
@@ -1489,6 +1897,39 @@ var CmcdReportRecorder = class {
1489
1897
  }
1490
1898
  };
1491
1899
 
1900
+ //#endregion
1901
+ //#region src/CMCD_STATE_EVENT_FIELDS.ts
1902
+ /**
1903
+ * Maps each state-change event type to the persistent field whose value
1904
+ * the event signals.
1905
+ *
1906
+ * The key table (`CMCD_KEY_SPECS`) marks the field as always sent on its
1907
+ * event, so the key filter of `prepareCmcdData` does not remove it.
1908
+ * `CmcdReporter` deduplicates events against the field value. It drops an
1909
+ * event whose field has no value and restores the field if a transform
1910
+ * removes it.
1911
+ *
1912
+ * Sending the field with each state-change event is a library choice.
1913
+ * The exception is the exit from backgrounded mode, which CTA-5004-B
1914
+ * defines as a `b` event without `bg`. CTA-5004-B requires the field only
1915
+ * on the `ps` event, which MUST carry `sta`. CTA-5004-B states no such rule
1916
+ * for `pr`, `c`, or `bc`. `validateCmcdStructure` follows CTA-5004-B and
1917
+ * does not read this map.
1918
+ *
1919
+ * Iteration order matters: `CmcdReporter.update()` records state-change
1920
+ * events in map order when several tracked fields change in one call.
1921
+ * Do not reorder entries without auditing reporter behavior.
1922
+ *
1923
+ * @internal
1924
+ */
1925
+ const CMCD_STATE_EVENT_FIELDS = /* @__PURE__ */ new Map([
1926
+ [CMCD_EVENT_PLAY_STATE, "sta"],
1927
+ [CMCD_EVENT_PLAYBACK_RATE, "pr"],
1928
+ [CMCD_EVENT_CONTENT_ID, "cid"],
1929
+ [CMCD_EVENT_BACKGROUNDED_MODE, "bg"],
1930
+ [CMCD_EVENT_BITRATE_CHANGE, "br"]
1931
+ ]);
1932
+
1492
1933
  //#endregion
1493
1934
  //#region src/CmcdTransmissionMode.ts
1494
1935
  /**
@@ -1527,24 +1968,26 @@ const CmcdTransmissionMode = {
1527
1968
  //#endregion
1528
1969
  //#region src/upConvertToV2.ts
1529
1970
  /**
1530
- * Up-convert V1 CMCD data to V2 format.
1971
+ * Up-convert version 1 CMCD data to version 2.
1531
1972
  *
1532
- * - Wraps plain scalar values in arrays for inner-list keys.
1973
+ * - Wraps scalar values in arrays for inner-list keys.
1533
1974
  * - Wraps `nor` string in an array.
1534
1975
  *
1535
- * If the data is already V2 (has `v: 2`), it is returned unchanged.
1976
+ * The function returns version 2 data (has `v: 2`) unchanged. An inner list
1977
+ * with parameters, which `decodeCmcd` returns as an `SfItem` that wraps the
1978
+ * list, also stays unchanged.
1536
1979
  *
1537
1980
  * @internal
1538
1981
  */
1539
1982
  function upConvertToV2(obj) {
1540
- if (obj["v"] === CMCD_V2) return obj;
1983
+ if (toBareValue(obj["v"]) === CMCD_V2) return obj;
1541
1984
  const result = {};
1542
1985
  for (const [key, value] of Object.entries(obj)) {
1543
1986
  if (value == null) {
1544
1987
  result[key] = value;
1545
1988
  continue;
1546
1989
  }
1547
- if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
1990
+ if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(toBareValue(value))) result[key] = [value];
1548
1991
  else if (key === "nor" && typeof value === "string") result[key] = [value];
1549
1992
  else result[key] = value;
1550
1993
  }
@@ -1581,12 +2024,11 @@ function createEncodingOptions(reportingMode, config, baseUrl) {
1581
2024
  };
1582
2025
  }
1583
2026
  /**
1584
- * Deep equality for CmcdObjectTypeList (used for `br` dedup).
2027
+ * Deep equality for CmcdObjectTypeList (used for `br` deduplication).
1585
2028
  *
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.
2029
+ * Arrays with the same elements in different positions count as different.
2030
+ * Players that construct `br` consistently get correct deduplication.
2031
+ * Reordered arrays produce extra events, which is the safer failure.
1590
2032
  */
1591
2033
  function cmcdObjectTypeListEqual(a, b) {
1592
2034
  if (a === b) return true;
@@ -1595,10 +2037,9 @@ function cmcdObjectTypeListEqual(a, b) {
1595
2037
  const ai = a[i];
1596
2038
  const bi = b[i];
1597
2039
  if (ai === bi) continue;
1598
- if (typeof ai === "number" || typeof bi === "number") return false;
1599
- if (ai.value !== bi.value) return false;
1600
- const ap = ai.params;
1601
- const bp = bi.params;
2040
+ if ((typeof ai === "number" ? ai : ai.value) !== (typeof bi === "number" ? bi : bi.value)) return false;
2041
+ const ap = typeof ai === "number" ? void 0 : ai.params;
2042
+ const bp = typeof bi === "number" ? void 0 : bi.params;
1602
2043
  const ak = ap && Object.keys(ap)[0];
1603
2044
  const bk = bp && Object.keys(bp)[0];
1604
2045
  if (ak !== bk) return false;
@@ -1606,6 +2047,18 @@ function cmcdObjectTypeListEqual(a, b) {
1606
2047
  }
1607
2048
  return true;
1608
2049
  }
2050
+ /**
2051
+ * Equality for `br` deduplication. A `br` value can be an `SfItem` that
2052
+ * wraps the list, as `decodeCmcd` returns for an inner list with
2053
+ * parameters. Two values are equal when their lists are equal. The
2054
+ * parameters of the list are not compared.
2055
+ */
2056
+ function brEqual(a, b) {
2057
+ const listA = toBareValue(a);
2058
+ const listB = toBareValue(b);
2059
+ if (!Array.isArray(listA) || !Array.isArray(listB)) return Object.is(a, b);
2060
+ return cmcdObjectTypeListEqual(listA, listB);
2061
+ }
1609
2062
  const equal = Object.is;
1610
2063
  const identity = (v) => v;
1611
2064
  /**
@@ -1616,8 +2069,8 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
1616
2069
  if (field === "br") return {
1617
2070
  event,
1618
2071
  field,
1619
- equal: (a, b) => a === void 0 || b === void 0 ? a === b : cmcdObjectTypeListEqual(a, b),
1620
- snapshot: (v) => v.slice()
2072
+ equal: brEqual,
2073
+ snapshot: (v) => Array.isArray(v) ? v.slice() : copyItemValue(v)
1621
2074
  };
1622
2075
  return {
1623
2076
  event,
@@ -1627,57 +2080,48 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
1627
2080
  };
1628
2081
  });
1629
2082
  const STATE_FIELDS_BY_EVENT = /* @__PURE__ */ new Map(/* @__PURE__ */ STATE_FIELDS.map((e) => [e.event, e]));
2083
+ function buildRequiredEventKeys() {
2084
+ return new Map([
2085
+ ...CMCD_STATE_EVENT_FIELDS,
2086
+ [CMCD_EVENT_CUSTOM_EVENT, "cen"],
2087
+ [CMCD_EVENT_ERROR, "ec"],
2088
+ [CMCD_EVENT_RESPONSE_RECEIVED, "url"]
2089
+ ]);
2090
+ }
1630
2091
  /**
1631
- * Maps each event type to the key CTA-5004-B requires beyond `e` and `ts`.
1632
- * Built from the state-change table plus the three event types whose
1633
- * required key rides the caller's per-event data.
1634
- */
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
- ]);
1641
- /**
1642
- * Whether a required key's value will survive report preparation.
1643
- *
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.
2092
+ * Maps each event type to the key that a report of that type must keep,
2093
+ * beyond `e` and `ts`. Built from the state-change table plus the three
2094
+ * event types whose required key is event data, not player state.
1650
2095
  */
1651
- function isUsableRequiredValue(value) {
1652
- if (value == null || value === "") return false;
1653
- if (typeof value === "number") return Number.isFinite(value);
1654
- return !Array.isArray(value) || value.length > 0;
1655
- }
2096
+ const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ buildRequiredEventKeys();
1656
2097
  /**
1657
- * Copies an `SfItem`-shaped value and its `params` record.
2098
+ * Copies a value with the `SfItem` structure, including its `params` record
2099
+ * and the list of an `SfItem` that wraps a list.
1658
2100
  *
1659
- * The prototype is preserved because `prepareCmcdData`, the formatter map,
2101
+ * The copy keeps the prototype because `prepareCmcdData`, the formatter map,
1660
2102
  * validation, and the structured-field encoder all branch on
1661
2103
  * `instanceof SfItem`. A plain spread (and `structuredClone`) would return a
1662
- * prototype-less object and silently change what goes on the wire.
2104
+ * prototype-less object and silently change the encoded output.
1663
2105
  */
1664
2106
  function copyItemValue(value) {
1665
2107
  if (value === null || typeof value !== "object") return value;
1666
2108
  const copy = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
1667
2109
  if (copy.params !== null && typeof copy.params === "object") copy.params = { ...copy.params };
2110
+ if (Array.isArray(copy.value)) copy.value = copy.value.map(copyItemValue);
1668
2111
  return copy;
1669
2112
  }
1670
2113
  /**
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.
2114
+ * Copies the nested values of a report in place. A transform cannot mutate
2115
+ * the reporter's data store, or another target's report for the same event,
2116
+ * through an array or an `SfItem` it received.
1674
2117
  *
1675
- * Complete for the CMCD value space rather than best-effort: `CmcdValue` and
2118
+ * The copy is complete for the CMCD value space. `CmcdValue` and
1676
2119
  * `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.
2120
+ * those. `decodeCmcd` also returns an `SfItem` that wraps a list, and the
2121
+ * copy includes that list. `SfItem.params` is a flat record. The reporter calls this function
2122
+ * where a transform is configured, at session end on the ended session's
2123
+ * store, and on the request's stored player-facing view. The session-end
2124
+ * copy detaches the frozen snapshot from caller-held references.
1681
2125
  */
1682
2126
  function copyReportValues(data) {
1683
2127
  const record = data;
@@ -1692,14 +2136,13 @@ function copyReportValues(data) {
1692
2136
  return data;
1693
2137
  }
1694
2138
  /**
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.
2139
+ * Decodes the per-call data snapshot of a provenance record into new,
2140
+ * encodable report data. Tokens decode as `SfToken` and items with params
2141
+ * as `SfItem` (`useSymbol: false`), so every value re-encodes with its
2142
+ * original type. Only `recordResponseReceived()` calls this function.
2143
+ * Returns an empty object when the record is absent, has no snapshot, or
2144
+ * the snapshot does not parse (a value this reporter did not write). The
2145
+ * response then reports its derived keys over the session's data alone.
1703
2146
  */
1704
2147
  function decodeSnapshot(provenance) {
1705
2148
  const encoded = provenance !== null && typeof provenance === "object" ? provenance.data : void 0;
@@ -1711,10 +2154,10 @@ function decodeSnapshot(provenance) {
1711
2154
  }
1712
2155
  }
1713
2156
  /**
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.
2157
+ * Creates a session's frozen base provenance record: the issuing `sid`, and
2158
+ * the `cid` that is current at creation time. `update()` creates a new record
2159
+ * on every `cid` change. Requests issued before a mid-session content change
2160
+ * keep their original `cid`, while later requests get the new one.
1718
2161
  */
1719
2162
  function mintProvenance(sid, cid) {
1720
2163
  return Object.freeze(typeof cid === "string" && cid ? {
@@ -1753,12 +2196,12 @@ function createCmcdReporterConfig(config) {
1753
2196
  /**
1754
2197
  * The CMCD reporter.
1755
2198
  *
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`.
2199
+ * `C` describes the player's own `customData`, which the reporter passes to
2200
+ * each configured `transform`. TypeScript infers `C` from the configuration,
2201
+ * so an annotation on one `transform` types the request in every other
2202
+ * `transform`. The default leaves `customData` values `unknown`.
1760
2203
  *
1761
- * @typeParam C - The shape of the player's `customData`. Defaults to
2204
+ * @typeParam C - The type of the player's `customData`. Defaults to
1762
2205
  * `Record<string, unknown>`.
1763
2206
  *
1764
2207
  * @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 +2213,7 @@ var CmcdReporter = class {
1770
2213
  * Creates a new CMCD reporter.
1771
2214
  *
1772
2215
  * @param config - The configuration for the CMCD reporter.
1773
- * @param requester - The function to use to send the request.
2216
+ * @param requester - The function that sends the request.
1774
2217
  * The default is a simple wrapper around the
1775
2218
  * native `fetch` API.
1776
2219
  */
@@ -1788,8 +2231,8 @@ var CmcdReporter = class {
1788
2231
  this.requester = requester;
1789
2232
  }
1790
2233
  /**
1791
- * Creates the state for a new session: fresh counters, gates, queues and
1792
- * dedup baseline for every configured target.
2234
+ * Creates the state for a new session: initial counters, flags, queues, and
2235
+ * deduplication baseline for every configured target.
1793
2236
  */
1794
2237
  createSession(sid, data) {
1795
2238
  const eventTargets = /* @__PURE__ */ new Map();
@@ -1804,7 +2247,7 @@ var CmcdReporter = class {
1804
2247
  provenance: mintProvenance(sid, data.cid),
1805
2248
  data,
1806
2249
  msd: NaN,
1807
- lastEmitted: {},
2250
+ lastEmitted: toBareValue(data.bg) === true ? {} : { bg: false },
1808
2251
  eventTargets,
1809
2252
  requestTarget: {
1810
2253
  sn: 0,
@@ -1813,11 +2256,12 @@ var CmcdReporter = class {
1813
2256
  };
1814
2257
  }
1815
2258
  /**
1816
- * Starts the CMCD reporter. Called by the player when the reporter is enabled.
2259
+ * Starts the CMCD reporter. The player calls this method when it enables the
2260
+ * reporter.
1817
2261
  *
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().
2262
+ * This method fires an initial time-interval event immediately (synchronously),
2263
+ * before the first interval elapses. Populate the CMCD data (sid, cid, and
2264
+ * others) before calling start().
1821
2265
  */
1822
2266
  start() {
1823
2267
  this.started = true;
@@ -1836,9 +2280,9 @@ var CmcdReporter = class {
1836
2280
  if (failure) throw failure.error;
1837
2281
  }
1838
2282
  /**
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.
2283
+ * Starts the time-interval timer for an event target when its config
2284
+ * requires one. Returns whether a timer started. The callback reports into
2285
+ * whichever session is current when the timer fires.
1842
2286
  */
1843
2287
  armInterval(config) {
1844
2288
  if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return false;
@@ -1853,7 +2297,8 @@ var CmcdReporter = class {
1853
2297
  return true;
1854
2298
  }
1855
2299
  /**
1856
- * Stops the CMCD reporter. Called by the player when the reporter is disabled.
2300
+ * Stops the CMCD reporter. The player calls this method when it disables the
2301
+ * reporter.
1857
2302
  *
1858
2303
  * @param flush - Whether to flush the event targets.
1859
2304
  */
@@ -1863,8 +2308,8 @@ var CmcdReporter = class {
1863
2308
  for (const config of this.config.eventTargets) this.disarmInterval(config);
1864
2309
  }
1865
2310
  /**
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
2311
+ * Sends all outstanding event reports, regardless of the batch size or
2312
+ * interval. Use this method when the player is destroyed or a playback
1868
2313
  * session ends.
1869
2314
  */
1870
2315
  flush() {
@@ -1873,40 +2318,45 @@ var CmcdReporter = class {
1873
2318
  /**
1874
2319
  * Updates the CMCD data.
1875
2320
  *
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.
2321
+ * The player calls this method when data changes. The tracked state fields
2322
+ * are `sta`, `pr`, `cid`, `bg`, and `br`. If a new value for one of them
2323
+ * differs from the last reported value for that field, the reporter fires
2324
+ * the matching state-change event. The comparison uses the last reported
2325
+ * value, not the previous persisted value. The first state-change event in
2326
+ * a new session therefore always fires, even when the persisted value did
2327
+ * not change across the `sid` boundary.
1884
2328
  *
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.
2329
+ * The `bg` field is an exception. A session that does not start with
2330
+ * `bg: true` begins with `false` as its last reported `bg` value.
2331
+ * `bg: false` therefore fires `BACKGROUNDED_MODE` only if the session
2332
+ * started with `bg: true` or reported `bg: true`. CTA-5004-B defines a
2333
+ * `b` event without `bg` as the exit from backgrounded mode, so the exit
2334
+ * report does not carry `bg`. A target transform can add `bg: false` to
2335
+ * the exit report. The encoder writes that value as `?0`.
1888
2336
  *
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.
2337
+ * Multi-field updates fire the events in this order: `sta`, `pr`, `cid`,
2338
+ * `bg`, `br`. The order of keys in the input object does not affect the
2339
+ * firing order.
1898
2340
  *
1899
- * A `sid` change resets the dedup baseline.
2341
+ * A state-change event can include context fields, for example `bl`, `mtp`,
2342
+ * `pt`, `ltc`. Those fields must be in the data store before the state field
2343
+ * changes. Either pass them with the state field in the same `update()`
2344
+ * call, or persist them with earlier `update()` calls. An auto-fired event
2345
+ * reports the current content of the data store. `TIME_INTERVAL` events also
2346
+ * read the data store, and the caller cannot add per-call data to them. The
2347
+ * player must therefore update the fields it wants in periodic reports with
2348
+ * this method.
1900
2349
  *
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
2350
+ * A `sid` change resets the deduplication baseline.
2351
+ *
2352
+ * `sid` and `msd` are session-owned. The reporter tracks them itself and
2353
+ * writes them into every outgoing report, so this method is the only way
1903
2354
  * to change them. A per-call value on {@link CmcdReporter.recordEvent},
1904
2355
  * {@link CmcdReporter.createRequestReport}, or
1905
- * {@link CmcdReporter.recordResponseReceived} has no effect; response
1906
- * attribution is by the provenance record alone.
2356
+ * {@link CmcdReporter.recordResponseReceived} has no effect.
1907
2357
  * `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.
2358
+ * `999_999_999_999_999` (the RFC 8941 integer maximum). The reporter
2359
+ * rounds `msd` to the nearest integer and ignores invalid values.
1910
2360
  *
1911
2361
  * @param data - The data to update.
1912
2362
  */
@@ -1928,19 +2378,20 @@ var CmcdReporter = class {
1928
2378
  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
2379
  }
1930
2380
  /**
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.
2381
+ * Starts a new session. The reporter retains the current session for late
2382
+ * responses and creates a new current session. It then sends the ended
2383
+ * sessions' queued reports and removes the oldest sessions beyond the
2384
+ * configured retention.
1934
2385
  *
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.
2386
+ * The ended session's snapshot is frozen: the reporter detaches its value
2387
+ * graph, so nested values from earlier `update()` calls cannot mutate the
2388
+ * snapshot. The new session starts from a shallow copy of that graph, which
2389
+ * preserves the data store. All fields, including `cid`, `br`, and custom
2390
+ * keys, survive a `sid` change as before. CTA-5004-B expects a `sid` to be
2391
+ * unique per playback session, so a reused `sid` replaces the retained
2392
+ * session with that `sid`. The replaced session's remaining state is lost,
2393
+ * as if removed, and the reporter attributes its late responses to the
2394
+ * replacement.
1944
2395
  */
1945
2396
  startSession(sid) {
1946
2397
  this.session.data = copyReportValues({ ...this.session.data });
@@ -1957,59 +2408,57 @@ var CmcdReporter = class {
1957
2408
  }
1958
2409
  }
1959
2410
  /**
1960
- * Records an event. Called by the player when an event occurs.
2411
+ * Records an event. The player calls this method when an event occurs.
1961
2412
  *
1962
2413
  * For state-change events (`PLAY_STATE`, `PLAYBACK_RATE`, `CONTENT_ID`,
1963
2414
  * `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).
2415
+ * 1. Writes the state field from `data` (if present) into the reporter's
2416
+ * data store, as `update()` would.
2417
+ * 2. Discards the event if the state field has no value after that write
2418
+ * (never set, or cleared with `update({ field: undefined })`).
2419
+ * 3. Suppresses the event if the field's current value equals the
2420
+ * last reported value (no state transition). {@link CmcdReporter.update}
2421
+ * describes the `bg` value that a new session starts with.
1971
2422
  *
1972
- * For all other event types, the event is always emitted.
2423
+ * The reporter always records all other event types.
1973
2424
  *
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.
2425
+ * For state-change events, prefer {@link CmcdReporter.update}, also for
2426
+ * context fields in a combined call like
2427
+ * `update({ sta: 'p', bl: [3000], mtp: [8500] })`. After `update()`
2428
+ * auto-fires a state-change event, deduplication suppresses a
2429
+ * `recordEvent()` call for the same event, and that call's `data` is
2430
+ * silently lost.
1980
2431
  *
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.
2432
+ * Call `recordEvent()` directly for events whose data comes with the event
2433
+ * call. Examples: `CUSTOM_EVENT` with `cen`, `ERROR` with `ec`, ad lifecycle
2434
+ * events, `MUTE`/`UNMUTE`, `PLAYER_EXPAND`/`PLAYER_COLLAPSE`, `SKIP`. For
2435
+ * `RESPONSE_RECEIVED`, prefer {@link CmcdReporter.recordResponseReceived},
2436
+ * which derives the per-response fields automatically.
1987
2437
  *
1988
2438
  * @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.
2439
+ * @param data - Additional data to record with the event. This data applies
2440
+ * only to this event report. The exception is the state field
2441
+ * of a state-change event, which the reporter also writes into
2442
+ * its data store. The reporter ignores session-owned keys
2443
+ * (`sid`, `msd`) supplied here and writes its own values.
1995
2444
  */
1996
2445
  recordEvent(type, data = {}) {
1997
2446
  this.emitEvent(this.session, type, data);
1998
2447
  }
1999
2448
  /**
2000
- * Records an event across every configured target of a session, applying
2001
- * state-change dedup once before per-target fan-out.
2449
+ * Records an event for every configured target of a session. State-change
2450
+ * deduplication runs once, before the per-target loop.
2002
2451
  *
2003
2452
  * @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.
2453
+ * touches (data store, deduplication baseline, counters,
2454
+ * queues) is that session's. Only `recordResponseReceived()`
2455
+ * ever passes an ended session.
2007
2456
  * @param type - The type of event to record.
2008
2457
  * @param data - Additional data to record with the event.
2009
2458
  * @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`.
2459
+ * one exists. Only `recordResponseReceived()` sets this
2460
+ * parameter, which the reporter passes to each target's
2461
+ * `transform`.
2013
2462
  */
2014
2463
  emitEvent(session, type, data, request) {
2015
2464
  const entry = STATE_FIELDS_BY_EVENT.get(type);
@@ -2034,16 +2483,17 @@ var CmcdReporter = class {
2034
2483
  if (failure) throw failure.error;
2035
2484
  }
2036
2485
  /**
2037
- * Records an event for a target. Called by the reporter when an event occurs.
2486
+ * Records an event for a target. The reporter calls this method when an
2487
+ * event occurs.
2038
2488
  *
2039
2489
  * @param target - The target to record the event for.
2040
2490
  * @param config - The configuration for the target.
2041
2491
  * @param type - The type of event to record.
2042
2492
  * @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()`.
2493
+ * only applies to this event report. Use `update()` for
2494
+ * persistent data.
2045
2495
  * @param request - The media request that triggered the event, when
2046
- * one exists. Passed to the target's `transform`.
2496
+ * one exists. The target's `transform` receives it.
2047
2497
  */
2048
2498
  recordTargetEvent(session, target, config, type, data = {}, request) {
2049
2499
  if (target.disposed || !config.events.includes(type)) return;
@@ -2054,6 +2504,7 @@ var CmcdReporter = class {
2054
2504
  e: type,
2055
2505
  ts: data.ts ?? Date.now()
2056
2506
  };
2507
+ if (type === CMCD_EVENT_BACKGROUNDED_MODE && toBareValue(item.bg) === false) delete item.bg;
2057
2508
  const { transform } = config;
2058
2509
  if (!transform) {
2059
2510
  this.queueTargetEvent(session, target, config, item, type);
@@ -2065,20 +2516,19 @@ var CmcdReporter = class {
2065
2516
  const ts = item.ts;
2066
2517
  const report = transform(item, request);
2067
2518
  if (report == null) return;
2068
- if (!isUsableRequiredValue(report.ts)) report.ts = ts;
2069
- if (requiredKey && isUsableRequiredValue(requiredValue) && !isUsableRequiredValue(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
2519
+ if (!isValid(report.ts)) report.ts = ts;
2520
+ if (requiredKey && isValid(requiredValue) && !isValid(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
2070
2521
  this.queueTargetEvent(session, target, config, report, type);
2071
2522
  }
2072
2523
  /**
2073
- * Stamps the reporter-owned fields on a finished event report, encodes it,
2074
- * and pushes the wire line to the target's queue.
2524
+ * Writes the reporter-owned fields into a finished event report, encodes
2525
+ * the report, and pushes the encoded line to the target's queue.
2075
2526
  *
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.
2527
+ * This method runs after any transform. A transform cannot bypass the
2528
+ * target's `events` filter through `e`, break `sn` continuity, or replace
2529
+ * the session identity in `sid` and `msd`. Encoding here makes a report
2530
+ * that cannot serialize throw inside the recording call, instead of failing
2531
+ * the batch send and re-queueing forever.
2082
2532
  *
2083
2533
  * @param target - The target to queue the report for.
2084
2534
  * @param config - The configuration for the target.
@@ -2096,10 +2546,10 @@ var CmcdReporter = class {
2096
2546
  target.queue.push(encodeCmcd(report, createEncodingOptions(CMCD_EVENT_MODE, config)));
2097
2547
  }
2098
2548
  /**
2099
- * Records a response-received event. Called by the player when a media
2100
- * request response has been fully received.
2549
+ * Records a response-received event. The player calls this method when a
2550
+ * media request response has fully arrived.
2101
2551
  *
2102
- * This method automatically derives the `rr` event keys from the
2552
+ * This method derives these `rr` event keys automatically:
2103
2553
  *
2104
2554
  * - `url` - the original requested URL (before any redirects)
2105
2555
  * - `rc` - the HTTP response status code
@@ -2107,60 +2557,57 @@ var CmcdReporter = class {
2107
2557
  * - `ttfb` - time to first byte (from `resourceTiming.responseStart`)
2108
2558
  * - `ttlb` - time to last byte (from `resourceTiming.duration`)
2109
2559
  *
2110
- * Additional keys like `ttfbb`, `cmsdd`, `cmsds`, and `smrt` can be
2111
- * supplied via the `data` parameter if the player has access to them.
2560
+ * If the player has additional keys like `ttfbb`, `cmsdd`, `cmsds`, and
2561
+ * `smrt`, pass them in the `data` parameter.
2112
2562
  *
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.
2563
+ * The request's `customData` is generic, so a player can pass a request with
2564
+ * its own keys (for example `{ requestType: 'segment' }`) without a cast. The
2565
+ * reporter reads only the provenance record that
2566
+ * {@link CmcdReporter.createRequestReport} writes there. Every other key
2567
+ * stays unchanged and visible to an event target's `transform` through its
2568
+ * `request` argument.
2119
2569
  *
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}.
2570
+ * A reporter with a concrete `C` requires the request to satisfy that type.
2571
+ * TypeScript therefore rejects a call whose request the configured
2572
+ * transforms could not read. See {@link CmcdReporterCustomData}.
2123
2573
  *
2124
- * The event is attributed to the session that issued the request, and to
2125
- * nothing else: the provenance record that
2126
- * {@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.
2574
+ * The provenance record that {@link CmcdReporter.createRequestReport}
2575
+ * stored on the request's `customData` (under
2576
+ * {@link CMCD_REQUEST_PROVENANCE}) selects the session by `sid`. A record
2577
+ * that names no retained session drops the response (see
2578
+ * `CmcdReporterConfig.sessionRetention`). A request without a record reports
2579
+ * under the current session, as in version 2.4.0. The request-time data then
2580
+ * comes from `customData.cmcd`. A per-call `data.sid` cannot substitute. The
2581
+ * reporter accepts any record whose `sid` names a retained session. The
2582
+ * record may come from another reporter with the same session attributes,
2583
+ * or be hand-built. If the request has a record, a response that completes
2584
+ * after a `sid` change reports under its own retained session, with that
2585
+ * session's data snapshot and sequence numbers.
2139
2586
  *
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.
2587
+ * With a record, request-time report keys come from the record's encoded
2588
+ * per-call `data` snapshot, which the reporter decodes for each response.
2589
+ * The record's `cid` replaces the session's current `cid` in the report. A
2590
+ * response that completes after a mid-session content change therefore
2591
+ * reports its request-time values.
2145
2592
  *
2146
- * @typeParam RD - The `customData` this request carries. Defaults to the
2593
+ * @typeParam RD - The `customData` of this request. Defaults to the
2147
2594
  * reporter's own `C`.
2148
2595
  *
2149
2596
  * @param response - The HTTP response received.
2150
2597
  * @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.
2598
+ * Values provided here override the derived values.
2599
+ * The reporter ignores session-owned keys (`sid`, `msd`)
2600
+ * supplied here.
2154
2601
  */
2155
2602
  recordResponseReceived(response, data = {}) {
2156
2603
  const { request } = response;
2157
2604
  const url = data.url ?? request?.url;
2158
2605
  if (!url) return;
2159
2606
  const provenance = request.customData?.[CMCD_REQUEST_PROVENANCE];
2160
- const session = this.resolveSession(provenance);
2607
+ const session = provenance === void 0 ? this.session : this.resolveSession(provenance);
2161
2608
  if (!session) return;
2162
- const cmcd = decodeSnapshot(provenance);
2163
- const { cid } = provenance;
2609
+ const cmcd = provenance === void 0 ? { ...request.customData?.cmcd } : decodeSnapshot(provenance);
2610
+ const { cid } = provenance ?? {};
2164
2611
  const urlObj = new URL(url);
2165
2612
  urlObj.searchParams.delete(CMCD_PARAM);
2166
2613
  const derived = {
@@ -2183,13 +2630,13 @@ var CmcdReporter = class {
2183
2630
  }, request);
2184
2631
  }
2185
2632
  /**
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.
2633
+ * Resolves the session that a provenance record names. The record's `sid`
2634
+ * must name one of this reporter's retained sessions, or the reporter
2635
+ * discards the response. A record that names a removed or unknown `sid`
2636
+ * resolves nothing. Any other attribution would be wrong. The reporter reads
2637
+ * the `sid` as a plain property. A record
2638
+ * copied through JSON therefore resolves the same session, and a hand-built
2639
+ * record that names a retained session is accepted.
2193
2640
  */
2194
2641
  resolveSession(provenance) {
2195
2642
  if (provenance === null || typeof provenance !== "object") return;
@@ -2197,8 +2644,8 @@ var CmcdReporter = class {
2197
2644
  return typeof sid === "string" ? this.sessions.get(sid) : void 0;
2198
2645
  }
2199
2646
  /**
2200
- * Applies the CMCD request report data to the request. Called by the player
2201
- * before sending the request.
2647
+ * Applies the CMCD request report data to the request. The player calls this
2648
+ * method before sending the request.
2202
2649
  *
2203
2650
  * @param req - The request to apply the CMCD request report to.
2204
2651
  * @returns The request with the CMCD request report applied.
@@ -2209,23 +2656,22 @@ var CmcdReporter = class {
2209
2656
  return this.createRequestReport(req) ?? req;
2210
2657
  }
2211
2658
  /**
2212
- * Checks if the request reporting is enabled.
2659
+ * Checks whether request reporting is enabled.
2213
2660
  *
2214
- * @returns `true` if the request reporting is enabled, `false` otherwise.
2661
+ * @returns `true` if request reporting is enabled, `false` otherwise.
2215
2662
  */
2216
2663
  isRequestReportingEnabled() {
2217
2664
  return !!this.config.enabledKeys?.length;
2218
2665
  }
2219
2666
  /**
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.
2667
+ * The provenance record to write on a returned request. It is the session's
2668
+ * frozen base record, extended with the caller's per-call data encoded as a
2669
+ * CMCD string when there is any. Requests without per-call data share the
2670
+ * base record, so the reporter writes only one property. The snapshot never
2671
+ * contains reporter-written fields. A value that cannot encode contributes
2672
+ * no snapshot and does not fail the request. The response still attributes
2673
+ * and reports derived keys over session data, matching the tolerance of
2674
+ * `decodeSnapshot()` when reading.
2229
2675
  */
2230
2676
  createRequestProvenance(data, baseUrl) {
2231
2677
  const base = this.session.provenance;
@@ -2245,21 +2691,21 @@ var CmcdReporter = class {
2245
2691
  }
2246
2692
  }
2247
2693
  /**
2248
- * Creates a new request with the CMCD request report data applied. Called by the player
2249
- * before sending the request.
2694
+ * Creates a new request with the CMCD request report data applied. The
2695
+ * player calls this method before sending the request.
2250
2696
  *
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
2697
+ * A reporter with a concrete `C` requires the request's `customData` to
2698
+ * satisfy that type. TypeScript therefore rejects a call whose request the
2699
+ * configured transform could not read. See
2254
2700
  * {@link CmcdReporterCustomData}.
2255
2701
  *
2256
- * @typeParam R - The request being decorated. Its `customData` must satisfy
2702
+ * @typeParam R - The type of the request. Its `customData` must satisfy
2257
2703
  * the reporter's `C`.
2258
2704
  *
2259
2705
  * @param request - The request to apply the CMCD request report to.
2260
2706
  * @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()`.
2707
+ * applies to this request report. Use `update()` for
2708
+ * persistent data.
2263
2709
  * @returns The request with the CMCD request report applied.
2264
2710
  */
2265
2711
  createRequestReport(request, data) {
@@ -2311,7 +2757,8 @@ var CmcdReporter = class {
2311
2757
  return report;
2312
2758
  }
2313
2759
  /**
2314
- * Processes the event targets. Called by the reporter when an event occurs.
2760
+ * Processes the event targets. The reporter calls this method when an event
2761
+ * occurs.
2315
2762
  *
2316
2763
  * @param flush - Whether to flush the event targets.
2317
2764
  */
@@ -2334,7 +2781,8 @@ var CmcdReporter = class {
2334
2781
  if (reprocess) this.processEventTargets();
2335
2782
  }
2336
2783
  /**
2337
- * Sends an event report. Called by the reporter when a batch is ready to be sent.
2784
+ * Sends an event report. The reporter calls this method when a batch is
2785
+ * ready.
2338
2786
  *
2339
2787
  * @param config - The target config to send the event report to.
2340
2788
  * @param data - The encoded report lines to send in the event report.
@@ -2344,27 +2792,27 @@ var CmcdReporter = class {
2344
2792
  url: config.url,
2345
2793
  method: "POST",
2346
2794
  headers: { "Content-Type": CMCD_MIME_TYPE },
2347
- body: data.join("\n") + "\n"
2795
+ body: data.join("\n")
2348
2796
  });
2349
2797
  if (status === 410) this.disposeEventTarget(session, config);
2350
2798
  else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
2351
2799
  }
2352
2800
  /**
2353
2801
  * 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).
2802
+ * stored id. Safe to call when no timer is active (clearInterval(undefined)
2803
+ * does nothing).
2356
2804
  */
2357
2805
  disarmInterval(config) {
2358
2806
  clearInterval(this.intervals.get(config));
2359
2807
  this.intervals.delete(config);
2360
2808
  }
2361
2809
  /**
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.
2810
+ * Silences an event target URL for the remainder of its session after the
2811
+ * collector signals that the target is gone (HTTP 410). The method clears
2812
+ * the queues and blocks further enqueues for every config in the session
2813
+ * that reports to the URL (the CTA-5004-B suppression scope). For the
2814
+ * current session, the method also cancels those configs' timers. A session
2815
+ * started after the disposal gets new target state and is unaffected.
2368
2816
  */
2369
2817
  disposeEventTarget(session, config) {
2370
2818
  session.eventTargets.forEach((target, sibling) => {
@@ -2475,6 +2923,63 @@ function fromCmcdUrl(url, options) {
2475
2923
  return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")), options);
2476
2924
  }
2477
2925
 
2926
+ //#endregion
2927
+ //#region src/isCmcdRequestKey.ts
2928
+ const CMCD_REQUEST_KEY_SET = /* @__PURE__ */ new Set(CMCD_REQUEST_KEYS);
2929
+ /**
2930
+ * Check if a key is a valid CMCD request key.
2931
+ *
2932
+ * @param key - The key to check.
2933
+ *
2934
+ * @returns `true` if the key is a valid CMCD request key, `false` otherwise.
2935
+ *
2936
+ * @public
2937
+ *
2938
+ * @example
2939
+ * {@includeCode ../test/isCmcdRequestKey.test.ts#example}
2940
+ */
2941
+ function isCmcdRequestKey(key) {
2942
+ return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
2943
+ }
2944
+
2945
+ //#endregion
2946
+ //#region src/isCmcdResponseReceivedKey.ts
2947
+ const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
2948
+ /**
2949
+ * Check if a key is a valid CMCD response key.
2950
+ *
2951
+ * @param key - The key to check.
2952
+ *
2953
+ * @returns `true` if the key is a valid CMCD response key, `false` otherwise.
2954
+ *
2955
+ * @public
2956
+ *
2957
+ * @example
2958
+ * {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
2959
+ */
2960
+ function isCmcdResponseReceivedKey(key) {
2961
+ return CMCD_RESPONSE_KEY_SET.has(key);
2962
+ }
2963
+
2964
+ //#endregion
2965
+ //#region src/isCmcdEventKey.ts
2966
+ const CMCD_EVENT_KEY_SET = /* @__PURE__ */ new Set(CMCD_EVENT_KEYS);
2967
+ /**
2968
+ * Check if a key is a valid CMCD event key.
2969
+ *
2970
+ * @param key - The key to check.
2971
+ *
2972
+ * @returns `true` if the key is a valid CMCD event key, `false` otherwise.
2973
+ *
2974
+ * @public
2975
+ *
2976
+ * @example
2977
+ * {@includeCode ../test/isCmcdEventKey.test.ts#example}
2978
+ */
2979
+ function isCmcdEventKey(key) {
2980
+ return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
2981
+ }
2982
+
2478
2983
  //#endregion
2479
2984
  //#region src/isCmcdV1Data.ts
2480
2985
  /**
@@ -2493,6 +2998,25 @@ function isCmcdV1Data(data) {
2493
2998
  return data.v !== CMCD_V2;
2494
2999
  }
2495
3000
 
3001
+ //#endregion
3002
+ //#region src/isCmcdV1Key.ts
3003
+ const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
3004
+ /**
3005
+ * Filter for CMCD version 1 keys.
3006
+ *
3007
+ * @param key - The CMCD key to filter.
3008
+ *
3009
+ * @returns `true` if the key should be included, `false` otherwise.
3010
+ *
3011
+ * @public
3012
+ *
3013
+ * @example
3014
+ * {@includeCode ../test/isCmcdV1Key.test.ts#example}
3015
+ */
3016
+ function isCmcdV1Key(key) {
3017
+ return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
3018
+ }
3019
+
2496
3020
  //#endregion
2497
3021
  //#region src/isCmcdV2Data.ts
2498
3022
  /**
@@ -2517,7 +3041,7 @@ function isCmcdV2Data(data) {
2517
3041
  * Convert a value to a CMCD value.
2518
3042
  *
2519
3043
  * @param value - The value to convert to a CMCD value.
2520
- * @param params - The parameters to convert to a CMCD value.
3044
+ * @param params - The parameters of the CMCD value.
2521
3045
  * @returns The CMCD value.
2522
3046
  *
2523
3047
  * @public
@@ -2544,13 +3068,13 @@ function mergeValidationResults(...results) {
2544
3068
  //#endregion
2545
3069
  //#region src/resolveVersion.ts
2546
3070
  /**
2547
- * Resolves the CMCD version from explicit options, the payload's `v` key, or the default (v1).
3071
+ * Resolves the CMCD version from options, the payload's `v` key, or the default (version 1).
2548
3072
  *
2549
3073
  * @internal
2550
3074
  */
2551
3075
  function resolveVersion(data, options) {
2552
3076
  if (options?.version === 1 || options?.version === 2) return options.version;
2553
- const payloadVersion = data["v"];
3077
+ const payloadVersion = toBareValue(data["v"]);
2554
3078
  if (payloadVersion === 1 || payloadVersion === 2) return payloadVersion;
2555
3079
  return CMCD_V1;
2556
3080
  }
@@ -2591,6 +3115,51 @@ function validateCmcdKeys(data, options) {
2591
3115
  };
2592
3116
  }
2593
3117
 
3118
+ //#endregion
3119
+ //#region src/CMCD_AGGREGATE_BITRATE_KEYS.ts
3120
+ /**
3121
+ * Maps each aggregate bitrate key to the exact bitrate key that supersedes it.
3122
+ *
3123
+ * Per CTA-5004-B, `ab` MUST NOT be sent when the encoded bitrate `br` is
3124
+ * known. `lab` and `tab` follow the same rule for the lowest bitrate `lb`
3125
+ * and the top bitrate `tb`.
3126
+ *
3127
+ * @internal
3128
+ */
3129
+ const CMCD_AGGREGATE_BITRATE_KEYS = {
3130
+ ab: "br",
3131
+ lab: "lb",
3132
+ tab: "tb"
3133
+ };
3134
+
3135
+ //#endregion
3136
+ //#region src/CMCD_KEY_OBJECT_TYPES.ts
3137
+ /**
3138
+ * Maps a key to the object types (`ot`) for which CTA-5004-B allows the key.
3139
+ *
3140
+ * A key that is absent from this map is allowed for every object type.
3141
+ * `d` (Object duration) and `tpb` (Top playable bitrate) MUST NOT be sent for
3142
+ * other object types.
3143
+ *
3144
+ * @internal
3145
+ */
3146
+ const CMCD_KEY_OBJECT_TYPES = {
3147
+ d: [
3148
+ "a",
3149
+ "v",
3150
+ "av",
3151
+ "tt",
3152
+ "c",
3153
+ "o"
3154
+ ],
3155
+ tpb: [
3156
+ "a",
3157
+ "v",
3158
+ "av",
3159
+ "c"
3160
+ ]
3161
+ };
3162
+
2594
3163
  //#endregion
2595
3164
  //#region src/validateCmcdStructure.ts
2596
3165
  /**
@@ -2658,9 +3227,9 @@ function validateCmcdStructure(data, options) {
2658
3227
  message: `Response key "${key}" must only be present when e="rr".`,
2659
3228
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2660
3229
  });
2661
- for (const [stateEventType, requiredField] of CMCD_STATE_EVENT_FIELDS) if (eventType === stateEventType && !(requiredField in data)) issues.push({
2662
- key: requiredField,
2663
- message: `State-change event (e="${stateEventType}") requires the "${requiredField}" key to be present.`,
3230
+ if (eventType === CMCD_EVENT_PLAY_STATE && !("sta" in data)) issues.push({
3231
+ key: "sta",
3232
+ message: "Play state change event (e=\"ps\") requires the \"sta\" key to be present.",
2664
3233
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2665
3234
  });
2666
3235
  if (eventType === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
@@ -2669,9 +3238,27 @@ function validateCmcdStructure(data, options) {
2669
3238
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2670
3239
  });
2671
3240
  }
2672
- if ("v" in data && data["v"] !== 1 && data["v"] !== 2) issues.push({
3241
+ const ot = version > 1 ? toTokenString(data["ot"]) : void 0;
3242
+ if (ot !== void 0) for (const key in CMCD_KEY_OBJECT_TYPES) {
3243
+ const objectTypes = CMCD_KEY_OBJECT_TYPES[key];
3244
+ if (key in data && !objectTypes.includes(ot)) issues.push({
3245
+ key,
3246
+ message: `Key "${key}" must not be present when "ot" is "${ot}". Expected "ot" to be one of: ${objectTypes.join(", ")}.`,
3247
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3248
+ });
3249
+ }
3250
+ if (version > 1) for (const key in CMCD_AGGREGATE_BITRATE_KEYS) {
3251
+ const exactKey = CMCD_AGGREGATE_BITRATE_KEYS[key];
3252
+ if (key in data && exactKey in data) issues.push({
3253
+ key,
3254
+ message: `Key "${key}" must not be present when "${exactKey}" is present.`,
3255
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3256
+ });
3257
+ }
3258
+ const v = toBareValue(data["v"]);
3259
+ if ("v" in data && v !== 1 && v !== 2) issues.push({
2673
3260
  key: "v",
2674
- message: `Unsupported CMCD version "${String(data["v"])}". Expected 1 or 2.`,
3261
+ message: `Unsupported CMCD version "${String(v)}". Expected 1 or 2.`,
2675
3262
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2676
3263
  });
2677
3264
  else if (version > 1 && !("v" in data)) issues.push({
@@ -2693,7 +3280,7 @@ function validateCmcdStructure(data, options) {
2693
3280
  //#endregion
2694
3281
  //#region src/CMCD_KEY_TYPES.ts
2695
3282
  /**
2696
- * CMCD key value type: inner list of numbers with token identifiers.
3283
+ * CMCD key value type: inner list of integers with token identifiers.
2697
3284
  *
2698
3285
  * @internal
2699
3286
  */
@@ -2735,8 +3322,8 @@ const CMCD_KEY_TYPE_STRING = "string";
2735
3322
  */
2736
3323
  const CMCD_KEY_TYPE_TOKEN = "token";
2737
3324
  /**
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.
3325
+ * Maps each CMCD key to its value type for version 2.
3326
+ * CMCD_V1_KEY_TYPE_OVERRIDES handles the keys that differ between version 1 and version 2.
2740
3327
  *
2741
3328
  * @internal
2742
3329
  */
@@ -2794,15 +3381,15 @@ const CMCD_KEY_TYPES = {
2794
3381
  sta: CMCD_KEY_TYPE_TOKEN
2795
3382
  };
2796
3383
  /**
2797
- * Maps keys to their v1-specific types when they differ from v2.
3384
+ * Version 1 types of the keys that differ from version 2.
2798
3385
  *
2799
3386
  * @internal
2800
3387
  */
2801
3388
  const CMCD_V1_KEY_TYPE_OVERRIDES = {
2802
3389
  bl: CMCD_KEY_TYPE_INTEGER,
2803
- br: CMCD_KEY_TYPE_NUMBER,
3390
+ br: CMCD_KEY_TYPE_INTEGER,
2804
3391
  mtp: CMCD_KEY_TYPE_INTEGER,
2805
- tb: CMCD_KEY_TYPE_NUMBER,
3392
+ tb: CMCD_KEY_TYPE_INTEGER,
2806
3393
  nor: CMCD_KEY_TYPE_STRING
2807
3394
  };
2808
3395
 
@@ -2893,22 +3480,31 @@ const CMCD_TOKEN_VALUES = {
2893
3480
 
2894
3481
  //#endregion
2895
3482
  //#region src/validateCmcdValues.ts
2896
- const HUNDRED_ROUNDING_KEYS = /* @__PURE__ */ new Set([
2897
- "bl",
2898
- "dl",
2899
- "mtp",
2900
- "rtp",
2901
- "tbl"
2902
- ]);
2903
- const INTEGER_ROUNDING_KEYS = /* @__PURE__ */ new Set([
2904
- "br",
2905
- "d",
2906
- "tb"
2907
- ]);
3483
+ const HUNDRED_ROUNDING_UNITS = {
3484
+ bl: "ms",
3485
+ dl: "ms",
3486
+ mtp: "kbps",
3487
+ rtp: "kbps",
3488
+ tbl: "ms"
3489
+ };
2908
3490
  function isFiniteNumber(value) {
2909
3491
  return typeof value === "number" && Number.isFinite(value);
2910
3492
  }
2911
- function validateListValue(key, value, issues) {
3493
+ function isInteger(value) {
3494
+ return Number.isInteger(value);
3495
+ }
3496
+ function validateRounding(key, value, version, issues, index) {
3497
+ const unit = HUNDRED_ROUNDING_UNITS[key];
3498
+ if (!unit || value % 100 === 0) return;
3499
+ const should = key === "tbl" || key === "bl" && version !== CMCD_V1;
3500
+ const target = index === void 0 ? `Key "${key}"` : `Key "${key}" array element [${index}]`;
3501
+ issues.push({
3502
+ key,
3503
+ message: `${target} ${should ? "should" : "must"} be rounded to the nearest 100 ${unit}. Received ${value}.`,
3504
+ severity: should ? CMCD_VALIDATION_SEVERITY_WARNING : CMCD_VALIDATION_SEVERITY_ERROR
3505
+ });
3506
+ }
3507
+ function validateListValue(key, value, version, issues) {
2912
3508
  if (!Array.isArray(value)) {
2913
3509
  issues.push({
2914
3510
  key,
@@ -2918,18 +3514,13 @@ function validateListValue(key, value, issues) {
2918
3514
  return;
2919
3515
  }
2920
3516
  for (let i = 0; i < value.length; i++) {
2921
- const element = value[i];
2922
- if (element instanceof SfItem) {
2923
- if (!isFiniteNumber(element.value)) issues.push({
2924
- key,
2925
- message: `Key "${key}" array element [${i}] must be a finite number.`,
2926
- severity: CMCD_VALIDATION_SEVERITY_ERROR
2927
- });
2928
- } else if (!isFiniteNumber(element)) issues.push({
3517
+ const element = toBareValue(value[i]);
3518
+ if (!isInteger(element)) issues.push({
2929
3519
  key,
2930
- message: `Key "${key}" array element [${i}] must be a finite number.`,
3520
+ message: `Key "${key}" array element [${i}] must be a finite integer.`,
2931
3521
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2932
3522
  });
3523
+ else validateRounding(key, element, version, issues, i);
2933
3524
  }
2934
3525
  }
2935
3526
  function validateStringArrayValue(key, value, issues) {
@@ -2941,20 +3532,11 @@ function validateStringArrayValue(key, value, issues) {
2941
3532
  });
2942
3533
  return;
2943
3534
  }
2944
- for (let i = 0; i < value.length; i++) {
2945
- const element = value[i];
2946
- if (element instanceof SfItem) {
2947
- if (typeof element.value !== "string") issues.push({
2948
- key,
2949
- message: `Key "${key}" array element [${i}] must be a string.`,
2950
- severity: CMCD_VALIDATION_SEVERITY_ERROR
2951
- });
2952
- } else if (typeof element !== "string") issues.push({
2953
- key,
2954
- message: `Key "${key}" array element [${i}] must be a string.`,
2955
- severity: CMCD_VALIDATION_SEVERITY_ERROR
2956
- });
2957
- }
3535
+ for (let i = 0; i < value.length; i++) if (typeof toBareValue(value[i]) !== "string") issues.push({
3536
+ key,
3537
+ message: `Key "${key}" array element [${i}] must be a string.`,
3538
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3539
+ });
2958
3540
  }
2959
3541
  /**
2960
3542
  * Validates that all values in a CMCD payload conform to the expected types and constraints.
@@ -2973,7 +3555,8 @@ function validateStringArrayValue(key, value, issues) {
2973
3555
  function validateCmcdValues(data, options) {
2974
3556
  const version = resolveVersion(data, options);
2975
3557
  const issues = [];
2976
- for (const [key, value] of Object.entries(data)) {
3558
+ for (const [key, entry] of Object.entries(data)) {
3559
+ const value = toBareValue(entry);
2977
3560
  if (isCmcdCustomKey(key)) {
2978
3561
  if (typeof value !== "string") issues.push({
2979
3562
  key,
@@ -3000,16 +3583,12 @@ function validateCmcdValues(data, options) {
3000
3583
  if (version === CMCD_V1 && key in CMCD_V1_KEY_TYPE_OVERRIDES) expectedType = CMCD_V1_KEY_TYPE_OVERRIDES[key];
3001
3584
  switch (expectedType) {
3002
3585
  case CMCD_KEY_TYPE_INTEGER:
3003
- if (!isFiniteNumber(value) || !Number.isInteger(value)) issues.push({
3586
+ if (!isInteger(value)) issues.push({
3004
3587
  key,
3005
3588
  message: `Key "${key}" must be a finite integer.`,
3006
3589
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3007
3590
  });
3008
- else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
3009
- key,
3010
- message: `Key "${key}" should be rounded to the nearest 100.`,
3011
- severity: CMCD_VALIDATION_SEVERITY_WARNING
3012
- });
3591
+ else validateRounding(key, value, version, issues);
3013
3592
  break;
3014
3593
  case CMCD_KEY_TYPE_NUMBER:
3015
3594
  if (!isFiniteNumber(value)) issues.push({
@@ -3017,16 +3596,6 @@ function validateCmcdValues(data, options) {
3017
3596
  message: `Key "${key}" must be a finite number.`,
3018
3597
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3019
3598
  });
3020
- else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
3021
- key,
3022
- message: `Key "${key}" should be rounded to the nearest 100.`,
3023
- severity: CMCD_VALIDATION_SEVERITY_WARNING
3024
- });
3025
- else if (INTEGER_ROUNDING_KEYS.has(key) && !Number.isInteger(value)) issues.push({
3026
- key,
3027
- message: `Key "${key}" should be rounded to an integer.`,
3028
- severity: CMCD_VALIDATION_SEVERITY_WARNING
3029
- });
3030
3599
  break;
3031
3600
  case CMCD_KEY_TYPE_BOOLEAN:
3032
3601
  if (typeof value !== "boolean") issues.push({
@@ -3058,7 +3627,7 @@ function validateCmcdValues(data, options) {
3058
3627
  break;
3059
3628
  }
3060
3629
  case CMCD_KEY_TYPE_NUMBER_LIST:
3061
- validateListValue(key, value, issues);
3630
+ validateListValue(key, value, version, issues);
3062
3631
  break;
3063
3632
  case CMCD_KEY_TYPE_STRING_LIST:
3064
3633
  validateStringArrayValue(key, value, issues);
@@ -3098,8 +3667,8 @@ function validateCmcd(data, options) {
3098
3667
  *
3099
3668
  * This function decodes the string internally and validates it with
3100
3669
  * `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.
3670
+ * newline-separated events, for example an `application/cmcd` POST body.
3671
+ * The function validates each line independently and merges the results.
3103
3672
  *
3104
3673
  * @param cmcd - The raw CMCD-encoded string to validate. May contain
3105
3674
  * multiple newline-separated event lines.
@@ -3127,6 +3696,11 @@ function validateCmcdEvents(cmcd, options) {
3127
3696
  }],
3128
3697
  data: []
3129
3698
  };
3699
+ const bodyIssues = [];
3700
+ if (cmcd.endsWith("\n")) bodyIssues.push({
3701
+ message: "Event report body must not end with a line feed.",
3702
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3703
+ });
3130
3704
  const decodedLines = [];
3131
3705
  const lineResults = [];
3132
3706
  for (let i = 0; i < lines.length; i++) try {
@@ -3144,7 +3718,10 @@ function validateCmcdEvents(cmcd, options) {
3144
3718
  });
3145
3719
  }
3146
3720
  return {
3147
- ...mergeValidationResults(...lineResults),
3721
+ ...mergeValidationResults({
3722
+ valid: true,
3723
+ issues: bodyIssues
3724
+ }, ...lineResults),
3148
3725
  data: decodedLines
3149
3726
  };
3150
3727
  }
@@ -3157,8 +3734,8 @@ function validateCmcdEvents(cmcd, options) {
3157
3734
  * Accepts an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
3158
3735
  *
3159
3736
  * 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}.
3737
+ * the correct `Content-Type` header (`application/cmcd`). It also
3738
+ * validates the body content with {@link validateCmcdEvents}.
3162
3739
  *
3163
3740
  * @param request - An `HttpRequest` to validate.
3164
3741
  * @param options - Validation options (excluding `reportingMode`).
@@ -3224,15 +3801,15 @@ function validateCmcdEventReport(request, options) {
3224
3801
  /**
3225
3802
  * Validates CMCD HTTP headers by checking shard placement and payload validity.
3226
3803
  *
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.
3804
+ * This function accepts raw CMCD header strings and decodes each shard
3805
+ * internally. It verifies that each key is in its correct header shard.
3806
+ * The function then merges all shards and runs payload validation (keys,
3807
+ * values, and structure) on the merged data.
3231
3808
  *
3232
3809
  * @example
3233
3810
  * {@includeCode ../test/validateCmcdHeaders.test.ts#example}
3234
3811
  *
3235
- * @param headers - A `Headers` instance or a record of CMCD header fields to their raw encoded string values.
3812
+ * @param headers - A `Headers` instance or a record of CMCD header fields to raw encoded strings.
3236
3813
  * @param options - Validation options (excluding `reportingMode`).
3237
3814
  * @returns The validation result including decoded data.
3238
3815
  *
@@ -3292,10 +3869,12 @@ function validateCmcdHeaders(headers, options) {
3292
3869
  * {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request}
3293
3870
  * object or an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
3294
3871
  *
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.
3872
+ * The function checks the HTTP headers for CMCD data first. If the
3873
+ * request has CMCD headers, {@link validateCmcdHeaders} validates them,
3874
+ * including shard placement. Otherwise, the function validates the CMCD
3875
+ * query parameter of the URL. A request with CMCD data in both the
3876
+ * headers and the `CMCD` query parameter is an error. The function still
3877
+ * validates the headers and returns their data.
3299
3878
  *
3300
3879
  * @param request - A `Request` or `HttpRequest` to validate.
3301
3880
  * @param options - Validation options (excluding `reportingMode`).
@@ -3310,12 +3889,25 @@ function validateCmcdHeaders(headers, options) {
3310
3889
  */
3311
3890
  function validateCmcdRequest(request, options) {
3312
3891
  const headers = extractHeaderRecord(request.headers);
3313
- if (headers) return validateCmcdHeaders(headers, options);
3314
- const param = new URL(request.url).searchParams.get(CMCD_PARAM);
3892
+ const param = getCmcdQueryParam(request.url);
3893
+ if (headers) {
3894
+ const result = validateCmcdHeaders(headers, options);
3895
+ if (!param) return result;
3896
+ return {
3897
+ ...mergeValidationResults({
3898
+ valid: false,
3899
+ issues: [{
3900
+ message: "CMCD data found in both request headers and the \"CMCD\" query parameter. A request must use only one transmission mode.",
3901
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3902
+ }]
3903
+ }, result),
3904
+ data: result.data
3905
+ };
3906
+ }
3315
3907
  if (!param) return {
3316
3908
  valid: false,
3317
3909
  issues: [{
3318
- message: "No CMCD data found in request headers or query parameters.",
3910
+ 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
3911
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3320
3912
  }],
3321
3913
  data: {}
@@ -3341,6 +3933,18 @@ function validateCmcdRequest(request, options) {
3341
3933
  data
3342
3934
  };
3343
3935
  }
3936
+ const CMCD_IN_FRAGMENT = /(?:^|[?&])CMCD=/;
3937
+ function getCmcdQueryParam(url) {
3938
+ const hash = url.indexOf("#");
3939
+ const path = hash < 0 ? url : url.slice(0, hash);
3940
+ const start = path.indexOf("?");
3941
+ if (start < 0) return null;
3942
+ return new URLSearchParams(path.slice(start + 1)).get(CMCD_PARAM);
3943
+ }
3944
+ function hasCmcdInFragment(url) {
3945
+ const hash = url.indexOf("#");
3946
+ return hash >= 0 && CMCD_IN_FRAGMENT.test(url.slice(hash + 1));
3947
+ }
3344
3948
  function extractHeaderRecord(headers) {
3345
3949
  if (!headers) return;
3346
3950
  const h = ensureHeaders(headers);