@svta/cml-cmcd 2.7.0 → 2.8.1

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,462 +1,6 @@
1
1
  import { SfItem, SfToken, decodeSfDict, encodeSfDict, symbolToStr } from "@svta/cml-structured-field-values";
2
2
  import { getBaseUrl, urlToRelativePath, uuid } from "@svta/cml-utils";
3
3
 
4
- //#region src/CMCD_AGGREGATE_BITRATE_KEYS.ts
5
- /**
6
- * Maps each aggregate bitrate key to the exact bitrate key that supersedes it.
7
- *
8
- * Per CTA-5004-B, `ab` MUST NOT be sent when the encoded bitrate `br` is
9
- * known. `lab` and `tab` follow the same rule for the lowest bitrate `lb`
10
- * and the top bitrate `tb`.
11
- *
12
- * @internal
13
- */
14
- const CMCD_AGGREGATE_BITRATE_KEYS = {
15
- ab: "br",
16
- lab: "lb",
17
- tab: "tb"
18
- };
19
-
20
- //#endregion
21
- //#region src/CMCD_FORMATTER_MAP.ts
22
- const roundValue = (value) => {
23
- if (value instanceof SfItem) return new SfItem(Math.round(value.value), value.params);
24
- return Math.round(value);
25
- };
26
- const toRounded = (value) => {
27
- if (Array.isArray(value)) return value.map(roundValue);
28
- return roundValue(value);
29
- };
30
- const toUrlSafe = (value, options) => {
31
- if (Array.isArray(value)) return value.map((item) => toUrlSafe(item, options));
32
- if (value instanceof SfItem && typeof value.value === "string") return new SfItem(toUrlSafe(value.value, options), value.params);
33
- else {
34
- if (options.baseUrl) value = urlToRelativePath(value, getBaseUrl(options.baseUrl));
35
- return options.version === 1 ? encodeURIComponent(value) : value;
36
- }
37
- };
38
- const hundredValue = (value) => {
39
- if (value instanceof SfItem) return new SfItem(Math.round(value.value / 100) * 100, value.params);
40
- return Math.round(value / 100) * 100;
41
- };
42
- const toHundred = (value) => {
43
- if (Array.isArray(value)) return value.map(hundredValue);
44
- return hundredValue(value);
45
- };
46
- const nor = (value, options) => {
47
- let norValue = value;
48
- if (options.version >= 2) {
49
- if (value instanceof SfItem && typeof value.value === "string") norValue = new SfItem([value]);
50
- else if (typeof value === "string") norValue = [value];
51
- }
52
- return toUrlSafe(norValue, options);
53
- };
54
- /**
55
- * The default formatters for CMCD values.
56
- *
57
- * @public
58
- */
59
- const CMCD_FORMATTER_MAP = {
60
- br: toRounded,
61
- d: toRounded,
62
- bl: toHundred,
63
- dl: toHundred,
64
- mtp: toHundred,
65
- nor,
66
- rtp: toHundred,
67
- tb: toRounded
68
- };
69
-
70
- //#endregion
71
- //#region src/CMCD_KEY_OBJECT_TYPES.ts
72
- /**
73
- * Maps a key to the object types (`ot`) for which CTA-5004-B allows the key.
74
- *
75
- * A key that is absent from this map is allowed for every object type.
76
- * `d` (Object duration) and `tpb` (Top playable bitrate) MUST NOT be sent for
77
- * other object types.
78
- *
79
- * @internal
80
- */
81
- const CMCD_KEY_OBJECT_TYPES = {
82
- d: [
83
- "a",
84
- "v",
85
- "av",
86
- "tt",
87
- "c",
88
- "o"
89
- ],
90
- tpb: [
91
- "a",
92
- "v",
93
- "av",
94
- "c"
95
- ]
96
- };
97
-
98
- //#endregion
99
- //#region src/CMCD_V2.ts
100
- /**
101
- * CMCD Version 2
102
- *
103
- * @public
104
- */
105
- const CMCD_V2 = 2;
106
-
107
- //#endregion
108
- //#region src/CmcdEventType.ts
109
- /**
110
- * CMCD event type 'bc' (bitrate change).
111
- *
112
- * @public
113
- */
114
- const CMCD_EVENT_BITRATE_CHANGE = "bc";
115
- /**
116
- * CMCD event type 'ps' (play state change).
117
- *
118
- * @public
119
- */
120
- const CMCD_EVENT_PLAY_STATE = "ps";
121
- /**
122
- * CMCD event type 'pr' (playback rate change).
123
- *
124
- * @public
125
- */
126
- const CMCD_EVENT_PLAYBACK_RATE = "pr";
127
- /**
128
- * CMCD event type 'e' (error).
129
- *
130
- * @public
131
- */
132
- const CMCD_EVENT_ERROR = "e";
133
- /**
134
- * CMCD event type 't' (time interval).
135
- *
136
- * @public
137
- */
138
- const CMCD_EVENT_TIME_INTERVAL = "t";
139
- /**
140
- * CMCD event type 'c' (content ID).
141
- *
142
- * @public
143
- */
144
- const CMCD_EVENT_CONTENT_ID = "c";
145
- /**
146
- * CMCD event type 'b' (backgrounded mode).
147
- *
148
- * @public
149
- */
150
- const CMCD_EVENT_BACKGROUNDED_MODE = "b";
151
- /**
152
- * CMCD event type 'm' (mute).
153
- *
154
- * @public
155
- */
156
- const CMCD_EVENT_MUTE = "m";
157
- /**
158
- * CMCD event type 'um' (unmute).
159
- *
160
- * @public
161
- */
162
- const CMCD_EVENT_UNMUTE = "um";
163
- /**
164
- * CMCD event type 'pe' (player expand).
165
- *
166
- * @public
167
- */
168
- const CMCD_EVENT_PLAYER_EXPAND = "pe";
169
- /**
170
- * CMCD event type 'pc' (player collapse).
171
- *
172
- * @public
173
- */
174
- const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
175
- /**
176
- * CMCD event type 'rr' (response received).
177
- *
178
- * @public
179
- */
180
- const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
181
- /**
182
- * CMCD event type 'as' (ad start).
183
- *
184
- * @public
185
- */
186
- const CMCD_EVENT_AD_START = "as";
187
- /**
188
- * CMCD event type 'ae' (ad end).
189
- *
190
- * @public
191
- */
192
- const CMCD_EVENT_AD_END = "ae";
193
- /**
194
- * CMCD event type 'abs' (ad break start).
195
- *
196
- * @public
197
- */
198
- const CMCD_EVENT_AD_BREAK_START = "abs";
199
- /**
200
- * CMCD event type 'abe' (ad break end).
201
- *
202
- * @public
203
- */
204
- const CMCD_EVENT_AD_BREAK_END = "abe";
205
- /**
206
- * CMCD event type 'sk' (skip).
207
- *
208
- * @public
209
- */
210
- const CMCD_EVENT_SKIP = "sk";
211
- /**
212
- * CMCD event type 'ce' (custom event).
213
- *
214
- * @public
215
- */
216
- const CMCD_EVENT_CUSTOM_EVENT = "ce";
217
- /**
218
- * CMCD event types for the 'e' key (event mode).
219
- *
220
- * @enum
221
- *
222
- * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-B Event}
223
- *
224
- * @public
225
- */
226
- const CmcdEventType = {
227
- BITRATE_CHANGE: CMCD_EVENT_BITRATE_CHANGE,
228
- PLAY_STATE: CMCD_EVENT_PLAY_STATE,
229
- PLAYBACK_RATE: CMCD_EVENT_PLAYBACK_RATE,
230
- ERROR: CMCD_EVENT_ERROR,
231
- TIME_INTERVAL: CMCD_EVENT_TIME_INTERVAL,
232
- CONTENT_ID: CMCD_EVENT_CONTENT_ID,
233
- BACKGROUNDED_MODE: CMCD_EVENT_BACKGROUNDED_MODE,
234
- MUTE: CMCD_EVENT_MUTE,
235
- UNMUTE: CMCD_EVENT_UNMUTE,
236
- PLAYER_EXPAND: CMCD_EVENT_PLAYER_EXPAND,
237
- PLAYER_COLLAPSE: CMCD_EVENT_PLAYER_COLLAPSE,
238
- RESPONSE_RECEIVED: CMCD_EVENT_RESPONSE_RECEIVED,
239
- AD_START: CMCD_EVENT_AD_START,
240
- AD_END: CMCD_EVENT_AD_END,
241
- AD_BREAK_START: CMCD_EVENT_AD_BREAK_START,
242
- AD_BREAK_END: CMCD_EVENT_AD_BREAK_END,
243
- SKIP: CMCD_EVENT_SKIP,
244
- CUSTOM_EVENT: CMCD_EVENT_CUSTOM_EVENT
245
- };
246
-
247
- //#endregion
248
- //#region src/CMCD_STATE_EVENT_FIELDS.ts
249
- /**
250
- * Maps each state-change event type to the persistent field whose value
251
- * the event signals.
252
- *
253
- * Per CTA-5004-B, the state-change events `ps`, `pr`, `c`, `b`, `bc` are
254
- * state-transition markers. Each event must include the field whose value
255
- * it signals. Callers force-include the field after filtering
256
- * (`prepareCmcdData`), deduplicate against its value (`CmcdReporter`), and
257
- * check its presence in payloads (`validateCmcdStructure`).
258
- *
259
- * Iteration order matters: `CmcdReporter.update()` records state-change
260
- * events in map order when several tracked fields change in one call.
261
- * Do not reorder entries without auditing reporter behavior.
262
- *
263
- * @internal
264
- */
265
- const CMCD_STATE_EVENT_FIELDS = /* @__PURE__ */ new Map([
266
- [CMCD_EVENT_PLAY_STATE, "sta"],
267
- [CMCD_EVENT_PLAYBACK_RATE, "pr"],
268
- [CMCD_EVENT_CONTENT_ID, "cid"],
269
- [CMCD_EVENT_BACKGROUNDED_MODE, "bg"],
270
- [CMCD_EVENT_BITRATE_CHANGE, "br"]
271
- ]);
272
-
273
- //#endregion
274
- //#region src/CmcdReportingMode.ts
275
- /**
276
- * CMCD event mode variable name.
277
- *
278
- * @public
279
- */
280
- const CMCD_EVENT_MODE = "event";
281
- /**
282
- * CMCD request mode variable name.
283
- *
284
- * @public
285
- */
286
- const CMCD_REQUEST_MODE = "request";
287
- /**
288
- * CMCD reporting mode types.
289
- *
290
- * @enum
291
- *
292
- * @public
293
- */
294
- const CmcdReportingMode = {
295
- REQUEST: CMCD_REQUEST_MODE,
296
- EVENT: CMCD_EVENT_MODE
297
- };
298
-
299
- //#endregion
300
- //#region src/CMCD_EVENT_KEYS.ts
301
- /**
302
- * Defines the event-specific keys for CMCD (Common Media Client Data) version 2.
303
- *
304
- * @public
305
- */
306
- const CMCD_EVENT_KEYS = [
307
- "cen",
308
- "e",
309
- "h",
310
- "ts"
311
- ];
312
-
313
- //#endregion
314
- //#region src/CMCD_REQUEST_KEYS.ts
315
- /**
316
- * Defines the request-specific keys for CMCD (Common Media Client Data) version 2.
317
- *
318
- * @public
319
- */
320
- const CMCD_REQUEST_KEYS = [
321
- "ab",
322
- "bg",
323
- "bl",
324
- "br",
325
- "bs",
326
- "bsa",
327
- "bsd",
328
- "bsda",
329
- "cdn",
330
- "cid",
331
- "cs",
332
- "d",
333
- "dfa",
334
- "dl",
335
- "ec",
336
- "lab",
337
- "lb",
338
- "ltc",
339
- "msd",
340
- "mtp",
341
- "nor",
342
- "nr",
343
- "ot",
344
- "pb",
345
- "pr",
346
- "pt",
347
- "rtp",
348
- "sf",
349
- "sid",
350
- "sn",
351
- "st",
352
- "sta",
353
- "su",
354
- "tab",
355
- "tb",
356
- "tbl",
357
- "tpb",
358
- "v"
359
- ];
360
-
361
- //#endregion
362
- //#region src/isCmcdCustomKey.ts
363
- const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
364
- /**
365
- * Check if a key is a valid custom key: a lowercase first letter, then
366
- * characters from `a-z 0-9 . -`. The key needs a hyphen, not at the start
367
- * or end. This check applies the CTA-5004-B custom-key rules, restricted
368
- * to names that RFC 8941 key serialization accepts.
369
- *
370
- * @param key - The key to check.
371
- *
372
- * @returns `true` if the key is a custom key, `false` otherwise.
373
- *
374
- * @public
375
- *
376
- * @example
377
- * {@includeCode ../test/isCmcdCustomKey.test.ts#example}
378
- */
379
- function isCmcdCustomKey(key) {
380
- const separator = key.indexOf("-", 1);
381
- return separator > 0 && separator < key.length - 1 && CUSTOM_KEY_REGEX.test(key);
382
- }
383
-
384
- //#endregion
385
- //#region src/isCmcdRequestKey.ts
386
- const CMCD_REQUEST_KEY_SET = /* @__PURE__ */ new Set(CMCD_REQUEST_KEYS);
387
- /**
388
- * Check if a key is a valid CMCD request key.
389
- *
390
- * @param key - The key to check.
391
- *
392
- * @returns `true` if the key is a valid CMCD request key, `false` otherwise.
393
- *
394
- * @public
395
- *
396
- * @example
397
- * {@includeCode ../test/isCmcdRequestKey.test.ts#example}
398
- */
399
- function isCmcdRequestKey(key) {
400
- return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
401
- }
402
-
403
- //#endregion
404
- //#region src/CMCD_RESPONSE_KEYS.ts
405
- /**
406
- * Response-only and timing keys for version 2.
407
- *
408
- * @public
409
- */
410
- const CMCD_RESPONSE_KEYS = [
411
- "cmsdd",
412
- "cmsds",
413
- "rc",
414
- "smrt",
415
- "ttfb",
416
- "ttfbb",
417
- "ttlb",
418
- "url"
419
- ];
420
-
421
- //#endregion
422
- //#region src/isCmcdResponseReceivedKey.ts
423
- const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
424
- /**
425
- * Check if a key is a valid CMCD response key.
426
- *
427
- * @param key - The key to check.
428
- *
429
- * @returns `true` if the key is a valid CMCD response key, `false` otherwise.
430
- *
431
- * @public
432
- *
433
- * @example
434
- * {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
435
- */
436
- function isCmcdResponseReceivedKey(key) {
437
- return CMCD_RESPONSE_KEY_SET.has(key);
438
- }
439
-
440
- //#endregion
441
- //#region src/isCmcdEventKey.ts
442
- const CMCD_EVENT_KEY_SET = /* @__PURE__ */ new Set(CMCD_EVENT_KEYS);
443
- /**
444
- * Check if a key is a valid CMCD event key.
445
- *
446
- * @param key - The key to check.
447
- *
448
- * @returns `true` if the key is a valid CMCD event key, `false` otherwise.
449
- *
450
- * @public
451
- *
452
- * @example
453
- * {@includeCode ../test/isCmcdEventKey.test.ts#example}
454
- */
455
- function isCmcdEventKey(key) {
456
- return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
457
- }
458
-
459
- //#endregion
460
4
  //#region src/CMCD_INNER_LIST_KEYS.ts
461
5
  /**
462
6
  * Inner-list keys in version 2 that are scalars in version 1.
@@ -483,72 +27,424 @@ const CMCD_INNER_LIST_KEYS = new Set([
483
27
  ]);
484
28
 
485
29
  //#endregion
486
- //#region src/CMCD_V1_KEYS.ts
30
+ //#region src/CMCD_KEY_SPECS.ts
31
+ const MEDIA = [
32
+ "a",
33
+ "v",
34
+ "av",
35
+ "tt",
36
+ "c",
37
+ "o"
38
+ ];
487
39
  /**
488
- * Defines the keys for CMCD (Common Media Client Data) version 1.
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`.
41
+ *
42
+ * @internal
43
+ */
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
+ }
313
+ };
314
+
315
+ //#endregion
316
+ //#region src/CMCD_V2.ts
317
+ /**
318
+ * CMCD Version 2
489
319
  *
490
320
  * @public
491
321
  */
492
- const CMCD_V1_KEYS = [
493
- "bl",
494
- "br",
495
- "bs",
496
- "cid",
497
- "d",
498
- "dl",
499
- "mtp",
500
- "nor",
501
- "nrr",
502
- "ot",
503
- "pr",
504
- "rtp",
505
- "sf",
506
- "sid",
507
- "st",
508
- "su",
509
- "tb",
510
- "v"
511
- ];
322
+ const CMCD_V2 = 2;
512
323
 
513
324
  //#endregion
514
- //#region src/isCmcdV1Key.ts
515
- const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
325
+ //#region src/CmcdReportingMode.ts
516
326
  /**
517
- * Filter for CMCD version 1 keys.
327
+ * CMCD event mode variable name.
518
328
  *
519
- * @param key - The CMCD key to filter.
329
+ * @public
330
+ */
331
+ const CMCD_EVENT_MODE = "event";
332
+ /**
333
+ * CMCD request mode variable name.
520
334
  *
521
- * @returns `true` if the key should be included, `false` otherwise.
335
+ * @public
336
+ */
337
+ const CMCD_REQUEST_MODE = "request";
338
+ /**
339
+ * CMCD reporting mode types.
340
+ *
341
+ * @enum
522
342
  *
523
343
  * @public
344
+ */
345
+ const CmcdReportingMode = {
346
+ REQUEST: CMCD_REQUEST_MODE,
347
+ EVENT: CMCD_EVENT_MODE
348
+ };
349
+
350
+ //#endregion
351
+ //#region src/downConvertToV1.ts
352
+ function hasParams(params) {
353
+ for (const _ in params) return true;
354
+ return false;
355
+ }
356
+ /**
357
+ * Unwrap an inner list or SfItem value to a scalar.
524
358
  *
525
- * @example
526
- * {@includeCode ../test/isCmcdV1Key.test.ts#example}
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`.
527
362
  */
528
- function isCmcdV1Key(key) {
529
- return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
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
+ }
377
+ /**
378
+ * Down-convert version 2 CMCD data to version 1.
379
+ *
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
384
+ */
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;
530
403
  }
531
404
 
532
405
  //#endregion
533
- //#region src/isTokenField.ts
534
- const TOKEN_FIELDS = /* @__PURE__ */ new Set([
535
- "ot",
536
- "sf",
537
- "st",
538
- "e",
539
- "sta"
540
- ]);
406
+ //#region src/isCmcdCustomKey.ts
407
+ const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
541
408
  /**
542
- * Checks if the given key is a token field.
409
+ * Check if a key is a valid custom key: a lowercase first letter, then
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.
543
413
  *
544
414
  * @param key - The key to check.
545
415
  *
546
- * @returns `true` if the key is a token field.
416
+ * @returns `true` if the key is a custom key, `false` otherwise.
417
+ *
418
+ * @public
419
+ *
420
+ * @example
421
+ * {@includeCode ../test/isCmcdCustomKey.test.ts#example}
422
+ */
423
+ function isCmcdCustomKey(key) {
424
+ const separator = key.indexOf("-", 1);
425
+ return separator > 0 && separator < key.length - 1 && CUSTOM_KEY_REGEX.test(key);
426
+ }
427
+
428
+ //#endregion
429
+ //#region src/getKeySpec.ts
430
+ const CUSTOM_KEY_SPEC = {
431
+ type: "custom",
432
+ omitDefault: false,
433
+ max: 64,
434
+ v1Max: Infinity
435
+ };
436
+ /**
437
+ * Returns the row of a key in the key table.
438
+ *
439
+ * @param key - The key to look up.
440
+ *
441
+ * @returns The row of a reserved key, the custom row for a valid custom key, or `undefined`.
547
442
  *
548
443
  * @internal
549
444
  */
550
- function isTokenField(key) {
551
- return TOKEN_FIELDS.has(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;
552
448
  }
553
449
 
554
450
  //#endregion
@@ -572,6 +468,60 @@ function isValid(value) {
572
468
  return value != null && value !== "" && value !== false;
573
469
  }
574
470
 
471
+ //#endregion
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;
500
+ }
501
+ /**
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.
505
+ *
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.
508
+ *
509
+ * @returns The formatted value, or `undefined` when no entry is a non-empty string.
510
+ *
511
+ * @internal
512
+ */
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];
523
+ }
524
+
575
525
  //#endregion
576
526
  //#region src/toTokenString.ts
577
527
  /**
@@ -593,52 +543,128 @@ function toTokenString(value) {
593
543
  }
594
544
 
595
545
  //#endregion
596
- //#region src/prepareCmcdData.ts
597
- const filterMap = {
598
- event: isCmcdEventKey,
599
- request: isCmcdRequestKey
600
- };
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
+ }
601
591
  /**
602
- * Unwrap an inner list or SfItem value to a scalar.
592
+ * Applies the value rule of a key type from the key table.
593
+ *
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.
601
+ *
602
+ * @returns The value to send, or `undefined` when the value fails the rule.
603
+ *
604
+ * @internal
603
605
  */
604
- function unwrapValue(value, ot) {
605
- if (Array.isArray(value)) {
606
- let item;
607
- const otText = toTokenString(ot);
608
- if (otText) item = value.find((item$1) => toTokenString(item$1.params?.ot) === otText);
609
- if (!item) item = value[0];
610
- return unwrapValue(item);
611
- }
612
- if (value instanceof SfItem) return value.value;
613
- return value;
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);
614
610
  }
611
+
612
+ //#endregion
613
+ //#region src/toBareValue.ts
615
614
  /**
616
- * Down-convert version 2 CMCD data to version 1.
615
+ * Returns the value inside an `SfItem`, or the value itself.
617
616
  *
618
- * - Extracts `nrr` from the `nor` SfItem's `r` parameter.
619
- * - Unwraps inner-list values to scalars.
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.
619
+ *
620
+ * @param value - The value to read.
621
+ * @returns The value without its parameters.
622
+ *
623
+ * @internal
620
624
  */
621
- function downConvertToV1(obj) {
622
- const result = {};
623
- for (const [key, value] of Object.entries(obj)) {
624
- if (value == null) {
625
- result[key] = value;
626
- continue;
627
- }
628
- if (key === "nor") {
629
- const first = (Array.isArray(value) ? value : [value])[0];
630
- if (first instanceof SfItem) {
631
- result["nor"] = first.value;
632
- if (first.params?.r) result["nrr"] = first.params.r;
633
- } else result["nor"] = first;
634
- } else if (CMCD_INNER_LIST_KEYS.has(key)) result[key] = unwrapValue(value, obj["ot"]);
635
- else result[key] = value;
636
- }
637
- return result;
625
+ function toBareValue(value) {
626
+ return value instanceof SfItem ? value.value : value;
627
+ }
628
+
629
+ //#endregion
630
+ //#region src/prepareCmcdData.ts
631
+ function hasKey(spec, reportingMode, isV1) {
632
+ return (spec.mode === void 0 || spec.mode === reportingMode) && (spec.version === void 0 || spec.version === 1 === isV1);
633
+ }
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]);
638
657
  }
639
- function formatValue(key, value, options, formatterOptions) {
640
- const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
641
- return typeof formatter === "function" && isValid(value) ? formatter(value, formatterOptions) : value;
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);
666
+ }
667
+ return formatted;
642
668
  }
643
669
  /**
644
670
  * Convert a generic object to CMCD data.
@@ -654,61 +680,66 @@ function formatValue(key, value, options, formatterOptions) {
654
680
  function prepareCmcdData(obj, options = {}) {
655
681
  const results = {};
656
682
  if (obj == null || typeof obj !== "object") return results;
657
- const version = options.version || obj["v"] || CMCD_V2;
683
+ const version = options.version || toBareValue(obj["v"]) || CMCD_V2;
658
684
  const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
659
- const data = version === 1 ? downConvertToV1(obj) : obj;
660
- const eventType = toTokenString(data["e"]);
661
- const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode];
662
- let keys = Object.keys(data).filter(keyFilter);
663
- if (data["e"] && eventType !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
664
- const filter = options.filter;
665
- if (typeof filter === "function") keys = keys.filter(filter);
666
- const isEventMode = reportingMode === CMCD_EVENT_MODE;
667
- if (isEventMode) {
668
- if (!keys.includes("e") && data["e"] != null) keys.push("e");
669
- if (!keys.includes("ts")) keys.push("ts");
670
- if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
671
- if (!keys.includes("ec") && data["ec"] != null && eventType === CMCD_EVENT_ERROR) keys.push("ec");
672
- if (!keys.includes("url") && data["url"] != null && eventType === CMCD_EVENT_RESPONSE_RECEIVED) keys.push("url");
673
- const requiredField = eventType ? CMCD_STATE_EVENT_FIELDS.get(eventType) : void 0;
674
- if (requiredField && data[requiredField] != null && !keys.includes(requiredField)) keys.push(requiredField);
675
- }
676
- if (keys.length === 0) return results;
677
- if (version > 1 && !keys.includes("v")) keys.push("v");
685
+ const isV1 = version === 1;
686
+ const isEventReport = !isV1 && reportingMode === CMCD_EVENT_MODE;
678
687
  const formatterOptions = {
679
688
  version,
680
689
  reportingMode,
681
690
  baseUrl: options.baseUrl
682
691
  };
683
- keys.sort();
692
+ const filter = options.filter;
693
+ let objectTypeValue;
684
694
  let objectType;
685
- let objectTypeResolved = false;
686
- for (const key of keys) {
687
- let value = data[key];
688
- const exactKey = CMCD_AGGREGATE_BITRATE_KEYS[key];
689
- if (exactKey && keys.includes(exactKey) && isValid(formatValue(exactKey, data[exactKey], options, formatterOptions))) continue;
690
- const objectTypes = version > 1 ? CMCD_KEY_OBJECT_TYPES[key] : void 0;
691
- if (objectTypes) {
692
- if (!objectTypeResolved) {
693
- const ot = formatValue("ot", data["ot"], options, formatterOptions);
694
- objectType = isValid(ot) ? toTokenString(ot) : void 0;
695
- objectTypeResolved = true;
696
- }
697
- if (objectType !== void 0 && !objectTypes.includes(objectType)) continue;
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;
698
730
  }
699
- if (key === "cen" && eventType !== CMCD_EVENT_CUSTOM_EVENT) continue;
700
- value = formatValue(key, value, options, formatterOptions);
701
- if (key === "v") if (version === 1) continue;
702
- else value = version;
703
- if (key === "pr" && value === 1 && !(isEventMode && eventType === CMCD_EVENT_PLAYBACK_RATE)) continue;
704
- if (isEventMode && key === "ts" && !Number.isFinite(value)) value = Date.now();
705
- const isBgFalseTransition = isEventMode && value === false && key === "bg" && eventType === CMCD_EVENT_BACKGROUNDED_MODE;
706
- if (!isValid(value) && !isBgFalseTransition) continue;
707
- if (isTokenField(key)) {
708
- if (typeof value === "string") value = new SfToken(value);
709
- else if (value instanceof SfItem && typeof value.value === "string") value = new SfItem(new SfToken(value.value), value.params);
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();
710
738
  }
711
- 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;
712
743
  }
713
744
  return results;
714
745
  }
@@ -916,15 +947,6 @@ function appendCmcdHeaders(headers, cmcd, options) {
916
947
  return Object.assign(headers, toCmcdHeaders(cmcd, options));
917
948
  }
918
949
 
919
- //#endregion
920
- //#region src/CMCD_PARAM.ts
921
- /**
922
- * CMCD parameter name.
923
- *
924
- * @public
925
- */
926
- const CMCD_PARAM = "CMCD";
927
-
928
950
  //#endregion
929
951
  //#region src/encodePreparedCmcd.ts
930
952
  /**
@@ -962,53 +984,99 @@ function encodeCmcd(cmcd, options = {}) {
962
984
  }
963
985
 
964
986
  //#endregion
965
- //#region src/toCmcdUrl.ts
987
+ //#region src/CMCD_PARAM.ts
966
988
  /**
967
- * Convert a CMCD object to a URL encoded string.
989
+ * CMCD parameter name.
968
990
  *
969
- * @param cmcd - The CMCD object to convert.
970
- * @param options - Options for encoding.
991
+ * @public
992
+ */
993
+ const CMCD_PARAM = "CMCD";
994
+
995
+ //#endregion
996
+ //#region src/percentEncode.ts
997
+ const SUB_DELIMS_REGEX = /[!'()*]/g;
998
+ const SURROGATE_REGEX = /[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g;
999
+ function encodeSubDelim(char) {
1000
+ return `%${char.charCodeAt(0).toString(16).toUpperCase()}`;
1001
+ }
1002
+ function replaceUnpaired(match) {
1003
+ return match.length === 2 ? match : "�";
1004
+ }
1005
+ /**
1006
+ * Percent-encode a string for the `CMCD` query parameter.
971
1007
  *
972
- * @returns The URL encoded CMCD data.
1008
+ * Only the unreserved characters of RFC 3986 stay unencoded: letters, digits,
1009
+ * `-`, `.`, `_`, and `~`. The query examples of CTA-5004-B use this encoding.
1010
+ * An unpaired surrogate becomes U+FFFD.
973
1011
  *
974
- * @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}
1012
+ * @param value - The string to encode.
1013
+ * @returns The encoded string.
975
1014
  *
976
- * @public
1015
+ * @internal
977
1016
  */
978
- function toCmcdUrl(cmcd, options = {}) {
979
- if (!cmcd) return "";
980
- const params = encodeCmcd(cmcd, options);
981
- return encodeURIComponent(params);
1017
+ function percentEncode(value) {
1018
+ let encoded;
1019
+ try {
1020
+ encoded = encodeURIComponent(value);
1021
+ } catch {
1022
+ encoded = encodeURIComponent(value.replace(SURROGATE_REGEX, replaceUnpaired));
1023
+ }
1024
+ return encoded.replace(SUB_DELIMS_REGEX, encodeSubDelim);
982
1025
  }
983
1026
 
984
1027
  //#endregion
985
- //#region src/toCmcdQuery.ts
1028
+ //#region src/replaceCmcdParam.ts
1029
+ function isCmcdParam(param) {
1030
+ return param === CMCD_PARAM || param.startsWith(`${CMCD_PARAM}=`);
1031
+ }
986
1032
  /**
987
- * Convert a CMCD object to a query arg.
988
- *
989
- * @param cmcd - The CMCD object to convert.
990
- * @param options - Options for encoding.
991
- *
992
- * @returns The CMCD query arg.
1033
+ * Replace the `CMCD` query parameter of a URL.
993
1034
  *
994
- * @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}
1035
+ * The first `CMCD` parameter takes the new value in place. The function
1036
+ * removes the other `CMCD` parameters. If the URL has no `CMCD` parameter,
1037
+ * the new parameter goes at the end of the query. An empty `value` only
1038
+ * removes the parameters. The function does not change the rest of the URL.
1039
+ * The URL can be relative.
995
1040
  *
996
- * @public
1041
+ * @param url - The URL.
1042
+ * @param value - The encoded CMCD data, before percent-encoding.
1043
+ * @returns The URL with at most one `CMCD` parameter.
997
1044
  *
998
- * @example
999
- * {@includeCode ../test/toCmcdQuery.test.ts#example}
1045
+ * @internal
1000
1046
  */
1001
- function toCmcdQuery(cmcd, options = {}) {
1002
- if (!cmcd) return "";
1003
- return `${CMCD_PARAM}=${toCmcdUrl(cmcd, options)}`;
1047
+ function replaceCmcdParam(url, value) {
1048
+ const hashIndex = url.indexOf("#");
1049
+ const end = hashIndex === -1 ? url.length : hashIndex;
1050
+ const queryIndex = url.indexOf("?");
1051
+ const start = queryIndex === -1 || queryIndex > end ? end : queryIndex;
1052
+ const params = [];
1053
+ let param = value ? `${CMCD_PARAM}=${percentEncode(value)}` : "";
1054
+ let found = false;
1055
+ for (const item of start < end ? url.slice(start + 1, end).split("&") : []) if (!isCmcdParam(item)) params.push(item);
1056
+ else {
1057
+ found = true;
1058
+ if (param) {
1059
+ params.push(param);
1060
+ param = "";
1061
+ }
1062
+ }
1063
+ if (param) {
1064
+ if (params[params.length - 1] === "") params.pop();
1065
+ params.push(param);
1066
+ } else if (!found) return url;
1067
+ const query = params.join("&");
1068
+ return `${url.slice(0, start)}${query ? `?${query}` : ""}${url.slice(end)}`;
1004
1069
  }
1005
1070
 
1006
1071
  //#endregion
1007
1072
  //#region src/appendCmcdQuery.ts
1008
- const REGEX = /CMCD=[^&#]+/;
1009
1073
  /**
1010
1074
  * Append CMCD query args to a URL.
1011
1075
  *
1076
+ * If the URL has a `CMCD` parameter, the new value replaces it in place. The
1077
+ * other parameters and the fragment stay as they are. If the data has no keys
1078
+ * to send, the function returns the URL unchanged.
1079
+ *
1012
1080
  * @param url - The URL to append to.
1013
1081
  * @param cmcd - The CMCD object to append.
1014
1082
  * @param options - Options for encoding.
@@ -1023,10 +1091,8 @@ const REGEX = /CMCD=[^&#]+/;
1023
1091
  * @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}
1024
1092
  */
1025
1093
  function appendCmcdQuery(url, cmcd, options) {
1026
- const query = toCmcdQuery(cmcd, options);
1027
- if (!query) return url;
1028
- if (REGEX.test(url)) return url.replace(REGEX, query);
1029
- return `${url}${url.includes("?") ? "&" : "?"}${query}`;
1094
+ const value = cmcd ? encodeCmcd(cmcd, options) : "";
1095
+ return value ? replaceCmcdParam(url, value) : url;
1030
1096
  }
1031
1097
 
1032
1098
  //#endregion
@@ -1038,6 +1104,176 @@ function appendCmcdQuery(url, cmcd, options) {
1038
1104
  */
1039
1105
  const CMCD_DEFAULT_TIME_INTERVAL = 30;
1040
1106
 
1107
+ //#endregion
1108
+ //#region src/CMCD_EVENT_KEYS.ts
1109
+ /**
1110
+ * Defines the event-specific keys for CMCD (Common Media Client Data) version 2.
1111
+ *
1112
+ * @public
1113
+ */
1114
+ const CMCD_EVENT_KEYS = [
1115
+ "cen",
1116
+ "e",
1117
+ "h",
1118
+ "ts"
1119
+ ];
1120
+
1121
+ //#endregion
1122
+ //#region src/CMCD_FORMATTER_MAP.ts
1123
+ const formatNumbers = (value, format) => {
1124
+ if (Array.isArray(value)) {
1125
+ const list = [];
1126
+ for (const item of value) {
1127
+ const formatted = formatNumbers(item, format);
1128
+ if (Number.isFinite(formatted instanceof SfItem ? formatted.value : formatted)) list.push(formatted);
1129
+ }
1130
+ return list;
1131
+ }
1132
+ if (value instanceof SfItem) {
1133
+ const formatted = formatNumbers(value.value, format);
1134
+ return isValid(formatted) ? new SfItem(formatted, value.params) : NaN;
1135
+ }
1136
+ return typeof value === "number" ? format(value) : NaN;
1137
+ };
1138
+ const toRounded = (value) => formatNumbers(value, Math.round);
1139
+ const toNumber = (value) => formatNumbers(value, Number);
1140
+ const roundToHundred = (value) => Math.round(value / 100) * 100;
1141
+ const toHundred = (value) => formatNumbers(value, roundToHundred);
1142
+ /**
1143
+ * Formatters for CMCD values.
1144
+ *
1145
+ * The encoder applies the same rounding through its key table, and `nor` is the `nor` rule of the encoder.
1146
+ * Use an entry to build a custom formatter for `CmcdEncodeOptions.formatters`.
1147
+ * The formatter of a numeric key drops a value or a list element that is not a finite number.
1148
+ *
1149
+ * @public
1150
+ */
1151
+ const CMCD_FORMATTER_MAP = {
1152
+ br: toRounded,
1153
+ d: toRounded,
1154
+ bl: toHundred,
1155
+ dl: toHundred,
1156
+ mtp: toHundred,
1157
+ nor: (value, options) => formatNor(value, options) ?? "",
1158
+ rtp: toHundred,
1159
+ tb: toRounded,
1160
+ tbl: toHundred,
1161
+ ab: toRounded,
1162
+ bsa: toRounded,
1163
+ bsd: toRounded,
1164
+ bsda: toRounded,
1165
+ dfa: toRounded,
1166
+ lab: toRounded,
1167
+ lb: toRounded,
1168
+ ltc: toRounded,
1169
+ msd: toRounded,
1170
+ pb: toRounded,
1171
+ pr: toNumber,
1172
+ pt: toRounded,
1173
+ rc: toRounded,
1174
+ sn: toRounded,
1175
+ tab: toRounded,
1176
+ tpb: toRounded,
1177
+ ts: toRounded,
1178
+ ttfb: toRounded,
1179
+ ttfbb: toRounded,
1180
+ ttlb: toRounded
1181
+ };
1182
+
1183
+ //#endregion
1184
+ //#region src/CMCD_REQUEST_KEYS.ts
1185
+ /**
1186
+ * Defines the request-specific keys for CMCD (Common Media Client Data) version 2.
1187
+ *
1188
+ * @public
1189
+ */
1190
+ const CMCD_REQUEST_KEYS = [
1191
+ "ab",
1192
+ "bg",
1193
+ "bl",
1194
+ "br",
1195
+ "bs",
1196
+ "bsa",
1197
+ "bsd",
1198
+ "bsda",
1199
+ "cdn",
1200
+ "cid",
1201
+ "cs",
1202
+ "d",
1203
+ "dfa",
1204
+ "dl",
1205
+ "ec",
1206
+ "lab",
1207
+ "lb",
1208
+ "ltc",
1209
+ "msd",
1210
+ "mtp",
1211
+ "nor",
1212
+ "nr",
1213
+ "ot",
1214
+ "pb",
1215
+ "pr",
1216
+ "pt",
1217
+ "rtp",
1218
+ "sf",
1219
+ "sid",
1220
+ "sn",
1221
+ "st",
1222
+ "sta",
1223
+ "su",
1224
+ "tab",
1225
+ "tb",
1226
+ "tbl",
1227
+ "tpb",
1228
+ "v"
1229
+ ];
1230
+
1231
+ //#endregion
1232
+ //#region src/CMCD_RESPONSE_KEYS.ts
1233
+ /**
1234
+ * Response-only and timing keys for version 2.
1235
+ *
1236
+ * @public
1237
+ */
1238
+ const CMCD_RESPONSE_KEYS = [
1239
+ "cmsdd",
1240
+ "cmsds",
1241
+ "rc",
1242
+ "smrt",
1243
+ "ttfb",
1244
+ "ttfbb",
1245
+ "ttlb",
1246
+ "url"
1247
+ ];
1248
+
1249
+ //#endregion
1250
+ //#region src/CMCD_V1_KEYS.ts
1251
+ /**
1252
+ * Defines the keys for CMCD (Common Media Client Data) version 1.
1253
+ *
1254
+ * @public
1255
+ */
1256
+ const CMCD_V1_KEYS = [
1257
+ "bl",
1258
+ "br",
1259
+ "bs",
1260
+ "cid",
1261
+ "d",
1262
+ "dl",
1263
+ "mtp",
1264
+ "nor",
1265
+ "nrr",
1266
+ "ot",
1267
+ "pr",
1268
+ "rtp",
1269
+ "sf",
1270
+ "sid",
1271
+ "st",
1272
+ "su",
1273
+ "tb",
1274
+ "v"
1275
+ ];
1276
+
1041
1277
  //#endregion
1042
1278
  //#region src/CMCD_KEYS.ts
1043
1279
  /**
@@ -1087,16 +1323,156 @@ const CMCD_MIME_TYPE = "application/cmcd";
1087
1323
  *
1088
1324
  * @public
1089
1325
  */
1090
- const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
1091
-
1092
- //#endregion
1093
- //#region src/CMCD_V1.ts
1326
+ const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
1327
+
1328
+ //#endregion
1329
+ //#region src/CMCD_V1.ts
1330
+ /**
1331
+ * CMCD Version 1
1332
+ *
1333
+ * @public
1334
+ */
1335
+ const CMCD_V1 = 1;
1336
+
1337
+ //#endregion
1338
+ //#region src/CmcdEventType.ts
1339
+ /**
1340
+ * CMCD event type 'bc' (bitrate change).
1341
+ *
1342
+ * @public
1343
+ */
1344
+ const CMCD_EVENT_BITRATE_CHANGE = "bc";
1345
+ /**
1346
+ * CMCD event type 'ps' (play state change).
1347
+ *
1348
+ * @public
1349
+ */
1350
+ const CMCD_EVENT_PLAY_STATE = "ps";
1351
+ /**
1352
+ * CMCD event type 'pr' (playback rate change).
1353
+ *
1354
+ * @public
1355
+ */
1356
+ const CMCD_EVENT_PLAYBACK_RATE = "pr";
1357
+ /**
1358
+ * CMCD event type 'e' (error).
1359
+ *
1360
+ * @public
1361
+ */
1362
+ const CMCD_EVENT_ERROR = "e";
1363
+ /**
1364
+ * CMCD event type 't' (time interval).
1365
+ *
1366
+ * @public
1367
+ */
1368
+ const CMCD_EVENT_TIME_INTERVAL = "t";
1369
+ /**
1370
+ * CMCD event type 'c' (content ID).
1371
+ *
1372
+ * @public
1373
+ */
1374
+ const CMCD_EVENT_CONTENT_ID = "c";
1375
+ /**
1376
+ * CMCD event type 'b' (backgrounded mode).
1377
+ *
1378
+ * @public
1379
+ */
1380
+ const CMCD_EVENT_BACKGROUNDED_MODE = "b";
1381
+ /**
1382
+ * CMCD event type 'm' (mute).
1383
+ *
1384
+ * @public
1385
+ */
1386
+ const CMCD_EVENT_MUTE = "m";
1387
+ /**
1388
+ * CMCD event type 'um' (unmute).
1389
+ *
1390
+ * @public
1391
+ */
1392
+ const CMCD_EVENT_UNMUTE = "um";
1393
+ /**
1394
+ * CMCD event type 'pe' (player expand).
1395
+ *
1396
+ * @public
1397
+ */
1398
+ const CMCD_EVENT_PLAYER_EXPAND = "pe";
1399
+ /**
1400
+ * CMCD event type 'pc' (player collapse).
1401
+ *
1402
+ * @public
1403
+ */
1404
+ const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
1405
+ /**
1406
+ * CMCD event type 'rr' (response received).
1407
+ *
1408
+ * @public
1409
+ */
1410
+ const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
1411
+ /**
1412
+ * CMCD event type 'as' (ad start).
1413
+ *
1414
+ * @public
1415
+ */
1416
+ const CMCD_EVENT_AD_START = "as";
1417
+ /**
1418
+ * CMCD event type 'ae' (ad end).
1419
+ *
1420
+ * @public
1421
+ */
1422
+ const CMCD_EVENT_AD_END = "ae";
1423
+ /**
1424
+ * CMCD event type 'abs' (ad break start).
1425
+ *
1426
+ * @public
1427
+ */
1428
+ const CMCD_EVENT_AD_BREAK_START = "abs";
1429
+ /**
1430
+ * CMCD event type 'abe' (ad break end).
1431
+ *
1432
+ * @public
1433
+ */
1434
+ const CMCD_EVENT_AD_BREAK_END = "abe";
1435
+ /**
1436
+ * CMCD event type 'sk' (skip).
1437
+ *
1438
+ * @public
1439
+ */
1440
+ const CMCD_EVENT_SKIP = "sk";
1441
+ /**
1442
+ * CMCD event type 'ce' (custom event).
1443
+ *
1444
+ * @public
1445
+ */
1446
+ const CMCD_EVENT_CUSTOM_EVENT = "ce";
1094
1447
  /**
1095
- * CMCD Version 1
1448
+ * CMCD event types for the 'e' key (event mode).
1449
+ *
1450
+ * @enum
1451
+ *
1452
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-B Event}
1096
1453
  *
1097
1454
  * @public
1098
1455
  */
1099
- const CMCD_V1 = 1;
1456
+ const CmcdEventType = {
1457
+ BITRATE_CHANGE: CMCD_EVENT_BITRATE_CHANGE,
1458
+ PLAY_STATE: CMCD_EVENT_PLAY_STATE,
1459
+ PLAYBACK_RATE: CMCD_EVENT_PLAYBACK_RATE,
1460
+ ERROR: CMCD_EVENT_ERROR,
1461
+ TIME_INTERVAL: CMCD_EVENT_TIME_INTERVAL,
1462
+ CONTENT_ID: CMCD_EVENT_CONTENT_ID,
1463
+ BACKGROUNDED_MODE: CMCD_EVENT_BACKGROUNDED_MODE,
1464
+ MUTE: CMCD_EVENT_MUTE,
1465
+ UNMUTE: CMCD_EVENT_UNMUTE,
1466
+ PLAYER_EXPAND: CMCD_EVENT_PLAYER_EXPAND,
1467
+ PLAYER_COLLAPSE: CMCD_EVENT_PLAYER_COLLAPSE,
1468
+ RESPONSE_RECEIVED: CMCD_EVENT_RESPONSE_RECEIVED,
1469
+ AD_START: CMCD_EVENT_AD_START,
1470
+ AD_END: CMCD_EVENT_AD_END,
1471
+ AD_BREAK_START: CMCD_EVENT_AD_BREAK_START,
1472
+ AD_BREAK_END: CMCD_EVENT_AD_BREAK_END,
1473
+ SKIP: CMCD_EVENT_SKIP,
1474
+ CUSTOM_EVENT: CMCD_EVENT_CUSTOM_EVENT
1475
+ };
1100
1476
 
1101
1477
  //#endregion
1102
1478
  //#region src/CmcdObjectType.ts
@@ -1556,6 +1932,39 @@ var CmcdReportRecorder = class {
1556
1932
  }
1557
1933
  };
1558
1934
 
1935
+ //#endregion
1936
+ //#region src/CMCD_STATE_EVENT_FIELDS.ts
1937
+ /**
1938
+ * Maps each state-change event type to the persistent field whose value
1939
+ * the event signals.
1940
+ *
1941
+ * The key table (`CMCD_KEY_SPECS`) marks the field as always sent on its
1942
+ * event, so the key filter of `prepareCmcdData` does not remove it.
1943
+ * `CmcdReporter` deduplicates events against the field value. It drops an
1944
+ * event whose field has no value and restores the field if a transform
1945
+ * removes it.
1946
+ *
1947
+ * Sending the field with each state-change event is a library choice.
1948
+ * The exception is the exit from backgrounded mode, which CTA-5004-B
1949
+ * defines as a `b` event without `bg`. CTA-5004-B requires the field only
1950
+ * on the `ps` event, which MUST carry `sta`. CTA-5004-B states no such rule
1951
+ * for `pr`, `c`, or `bc`. `validateCmcdStructure` follows CTA-5004-B and
1952
+ * does not read this map.
1953
+ *
1954
+ * Iteration order matters: `CmcdReporter.update()` records state-change
1955
+ * events in map order when several tracked fields change in one call.
1956
+ * Do not reorder entries without auditing reporter behavior.
1957
+ *
1958
+ * @internal
1959
+ */
1960
+ const CMCD_STATE_EVENT_FIELDS = /* @__PURE__ */ new Map([
1961
+ [CMCD_EVENT_PLAY_STATE, "sta"],
1962
+ [CMCD_EVENT_PLAYBACK_RATE, "pr"],
1963
+ [CMCD_EVENT_CONTENT_ID, "cid"],
1964
+ [CMCD_EVENT_BACKGROUNDED_MODE, "bg"],
1965
+ [CMCD_EVENT_BITRATE_CHANGE, "br"]
1966
+ ]);
1967
+
1559
1968
  //#endregion
1560
1969
  //#region src/CmcdTransmissionMode.ts
1561
1970
  /**
@@ -1599,19 +2008,21 @@ const CmcdTransmissionMode = {
1599
2008
  * - Wraps scalar values in arrays for inner-list keys.
1600
2009
  * - Wraps `nor` string in an array.
1601
2010
  *
1602
- * The function returns version 2 data (has `v: 2`) unchanged.
2011
+ * The function returns version 2 data (has `v: 2`) unchanged. An inner list
2012
+ * with parameters, which `decodeCmcd` returns as an `SfItem` that wraps the
2013
+ * list, also stays unchanged.
1603
2014
  *
1604
2015
  * @internal
1605
2016
  */
1606
2017
  function upConvertToV2(obj) {
1607
- if (obj["v"] === CMCD_V2) return obj;
2018
+ if (toBareValue(obj["v"]) === CMCD_V2) return obj;
1608
2019
  const result = {};
1609
2020
  for (const [key, value] of Object.entries(obj)) {
1610
2021
  if (value == null) {
1611
2022
  result[key] = value;
1612
2023
  continue;
1613
2024
  }
1614
- if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
2025
+ if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(toBareValue(value))) result[key] = [value];
1615
2026
  else if (key === "nor" && typeof value === "string") result[key] = [value];
1616
2027
  else result[key] = value;
1617
2028
  }
@@ -1661,10 +2072,9 @@ function cmcdObjectTypeListEqual(a, b) {
1661
2072
  const ai = a[i];
1662
2073
  const bi = b[i];
1663
2074
  if (ai === bi) continue;
1664
- if (typeof ai === "number" || typeof bi === "number") return false;
1665
- if (ai.value !== bi.value) return false;
1666
- const ap = ai.params;
1667
- const bp = bi.params;
2075
+ if ((typeof ai === "number" ? ai : ai.value) !== (typeof bi === "number" ? bi : bi.value)) return false;
2076
+ const ap = typeof ai === "number" ? void 0 : ai.params;
2077
+ const bp = typeof bi === "number" ? void 0 : bi.params;
1668
2078
  const ak = ap && Object.keys(ap)[0];
1669
2079
  const bk = bp && Object.keys(bp)[0];
1670
2080
  if (ak !== bk) return false;
@@ -1672,6 +2082,18 @@ function cmcdObjectTypeListEqual(a, b) {
1672
2082
  }
1673
2083
  return true;
1674
2084
  }
2085
+ /**
2086
+ * Equality for `br` deduplication. A `br` value can be an `SfItem` that
2087
+ * wraps the list, as `decodeCmcd` returns for an inner list with
2088
+ * parameters. Two values are equal when their lists are equal. The
2089
+ * parameters of the list are not compared.
2090
+ */
2091
+ function brEqual(a, b) {
2092
+ const listA = toBareValue(a);
2093
+ const listB = toBareValue(b);
2094
+ if (!Array.isArray(listA) || !Array.isArray(listB)) return Object.is(a, b);
2095
+ return cmcdObjectTypeListEqual(listA, listB);
2096
+ }
1675
2097
  const equal = Object.is;
1676
2098
  const identity = (v) => v;
1677
2099
  /**
@@ -1682,8 +2104,8 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
1682
2104
  if (field === "br") return {
1683
2105
  event,
1684
2106
  field,
1685
- equal: (a, b) => a === void 0 || b === void 0 ? a === b : cmcdObjectTypeListEqual(a, b),
1686
- snapshot: (v) => v.slice()
2107
+ equal: brEqual,
2108
+ snapshot: (v) => Array.isArray(v) ? v.slice() : copyItemValue(v)
1687
2109
  };
1688
2110
  return {
1689
2111
  event,
@@ -1702,29 +2124,14 @@ function buildRequiredEventKeys() {
1702
2124
  ]);
1703
2125
  }
1704
2126
  /**
1705
- * Maps each event type to the key CTA-5004-B requires beyond `e` and `ts`.
1706
- * Built from the state-change table plus the three event types whose
1707
- * required key is event data, not player state.
2127
+ * Maps each event type to the key that a report of that type must keep,
2128
+ * beyond `e` and `ts`. Built from the state-change table plus the three
2129
+ * event types whose required key is event data, not player state.
1708
2130
  */
1709
2131
  const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ buildRequiredEventKeys();
1710
2132
  /**
1711
- * Whether a required key's value will survive report preparation.
1712
- *
1713
- * This is `isValid` without its `false` exclusion. `false` must count as
1714
- * usable because `bg: false` is a valid value on a backgrounded-mode event,
1715
- * which the encoder writes as `?0`. If `false` counted as unusable,
1716
- * restoration would silently revert a transform that cleared the key.
1717
- * Later processing drops empty strings, empty lists, and non-finite numbers.
1718
- * A transform that substitutes one of them leaves the report without a
1719
- * required key.
1720
- */
1721
- function isUsableRequiredValue(value) {
1722
- if (value == null || value === "") return false;
1723
- if (typeof value === "number") return Number.isFinite(value);
1724
- return !Array.isArray(value) || value.length > 0;
1725
- }
1726
- /**
1727
- * Copies a value with the `SfItem` structure, including its `params` record.
2133
+ * Copies a value with the `SfItem` structure, including its `params` record
2134
+ * and the list of an `SfItem` that wraps a list.
1728
2135
  *
1729
2136
  * The copy keeps the prototype because `prepareCmcdData`, the formatter map,
1730
2137
  * validation, and the structured-field encoder all branch on
@@ -1735,6 +2142,7 @@ function copyItemValue(value) {
1735
2142
  if (value === null || typeof value !== "object") return value;
1736
2143
  const copy = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
1737
2144
  if (copy.params !== null && typeof copy.params === "object") copy.params = { ...copy.params };
2145
+ if (Array.isArray(copy.value)) copy.value = copy.value.map(copyItemValue);
1738
2146
  return copy;
1739
2147
  }
1740
2148
  /**
@@ -1744,7 +2152,8 @@ function copyItemValue(value) {
1744
2152
  *
1745
2153
  * The copy is complete for the CMCD value space. `CmcdValue` and
1746
2154
  * `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
1747
- * those. `SfItem.params` is a flat record. The reporter calls this function
2155
+ * those. `decodeCmcd` also returns an `SfItem` that wraps a list, and the
2156
+ * copy includes that list. `SfItem.params` is a flat record. The reporter calls this function
1748
2157
  * where a transform is configured, at session end on the ended session's
1749
2158
  * store, and on the request's stored player-facing view. The session-end
1750
2159
  * copy detaches the frozen snapshot from caller-held references.
@@ -1873,7 +2282,7 @@ var CmcdReporter = class {
1873
2282
  provenance: mintProvenance(sid, data.cid),
1874
2283
  data,
1875
2284
  msd: NaN,
1876
- lastEmitted: {},
2285
+ lastEmitted: toBareValue(data.bg) === true ? {} : { bg: false },
1877
2286
  eventTargets,
1878
2287
  requestTarget: {
1879
2288
  sn: 0,
@@ -1952,6 +2361,14 @@ var CmcdReporter = class {
1952
2361
  * a new session therefore always fires, even when the persisted value did
1953
2362
  * not change across the `sid` boundary.
1954
2363
  *
2364
+ * The `bg` field is an exception. A session that does not start with
2365
+ * `bg: true` begins with `false` as its last reported `bg` value.
2366
+ * `bg: false` therefore fires `BACKGROUNDED_MODE` only if the session
2367
+ * started with `bg: true` or reported `bg: true`. CTA-5004-B defines a
2368
+ * `b` event without `bg` as the exit from backgrounded mode, so the exit
2369
+ * report does not carry `bg`. A target transform can add `bg: false` to
2370
+ * the exit report. The encoder writes that value as `?0`.
2371
+ *
1955
2372
  * Multi-field updates fire the events in this order: `sta`, `pr`, `cid`,
1956
2373
  * `bg`, `br`. The order of keys in the input object does not affect the
1957
2374
  * firing order.
@@ -1971,8 +2388,7 @@ var CmcdReporter = class {
1971
2388
  * writes them into every outgoing report, so this method is the only way
1972
2389
  * to change them. A per-call value on {@link CmcdReporter.recordEvent},
1973
2390
  * {@link CmcdReporter.createRequestReport}, or
1974
- * {@link CmcdReporter.recordResponseReceived} has no effect. Response
1975
- * attribution uses the provenance record alone.
2391
+ * {@link CmcdReporter.recordResponseReceived} has no effect.
1976
2392
  * `msd` must be a finite number of milliseconds between `0` and
1977
2393
  * `999_999_999_999_999` (the RFC 8941 integer maximum). The reporter
1978
2394
  * rounds `msd` to the nearest integer and ignores invalid values.
@@ -2035,9 +2451,9 @@ var CmcdReporter = class {
2035
2451
  * data store, as `update()` would.
2036
2452
  * 2. Discards the event if the state field has no value after that write
2037
2453
  * (never set, or cleared with `update({ field: undefined })`).
2038
- * A state-change event without its required field would violate CTA-5004-B.
2039
2454
  * 3. Suppresses the event if the field's current value equals the
2040
- * last reported value (no state transition).
2455
+ * last reported value (no state transition). {@link CmcdReporter.update}
2456
+ * describes the `bg` value that a new session starts with.
2041
2457
  *
2042
2458
  * The reporter always records all other event types.
2043
2459
  *
@@ -2123,6 +2539,7 @@ var CmcdReporter = class {
2123
2539
  e: type,
2124
2540
  ts: data.ts ?? Date.now()
2125
2541
  };
2542
+ if (type === CMCD_EVENT_BACKGROUNDED_MODE && toBareValue(item.bg) === false) delete item.bg;
2126
2543
  const { transform } = config;
2127
2544
  if (!transform) {
2128
2545
  this.queueTargetEvent(session, target, config, item, type);
@@ -2134,8 +2551,8 @@ var CmcdReporter = class {
2134
2551
  const ts = item.ts;
2135
2552
  const report = transform(item, request);
2136
2553
  if (report == null) return;
2137
- if (!isUsableRequiredValue(report.ts)) report.ts = ts;
2138
- if (requiredKey && isUsableRequiredValue(requiredValue) && !isUsableRequiredValue(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
2554
+ if (!isValid(report.ts)) report.ts = ts;
2555
+ if (requiredKey && isValid(requiredValue) && !isValid(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
2139
2556
  this.queueTargetEvent(session, target, config, report, type);
2140
2557
  }
2141
2558
  /**
@@ -2189,27 +2606,24 @@ var CmcdReporter = class {
2189
2606
  * TypeScript therefore rejects a call whose request the configured
2190
2607
  * transforms could not read. See {@link CmcdReporterCustomData}.
2191
2608
  *
2192
- * The reporter attributes the event only to the session that issued the
2193
- * request. The provenance record that
2194
- * {@link CmcdReporter.createRequestReport} stored on the request's
2195
- * `customData` (under {@link CMCD_REQUEST_PROVENANCE}) selects the session
2196
- * by `sid`. Without a match, the reporter discards the response rather than
2197
- * attributing it elsewhere. There is no other key. The reporter discards the
2198
- * response when a serialization boundary lost the record and nothing
2199
- * restored it (see {@link CMCD_REQUEST_PROVENANCE}). It also discards the
2200
- * response when the session is no longer retained (see
2201
- * `CmcdReporterConfig.sessionRetention`). A per-call `data.sid` cannot
2202
- * substitute. The reporter accepts any record whose `sid` names a retained
2203
- * session. The record may come from another reporter with the same session
2204
- * attributes, or be hand-built. A response that completes after a `sid`
2205
- * change reports under its own retained session, with that session's data
2206
- * snapshot and sequence numbers.
2609
+ * The provenance record that {@link CmcdReporter.createRequestReport}
2610
+ * stored on the request's `customData` (under
2611
+ * {@link CMCD_REQUEST_PROVENANCE}) selects the session by `sid`. A record
2612
+ * that names no retained session drops the response (see
2613
+ * `CmcdReporterConfig.sessionRetention`). A request without a record reports
2614
+ * under the current session, as in version 2.4.0. The request-time data then
2615
+ * comes from `customData.cmcd`. A per-call `data.sid` cannot substitute. The
2616
+ * reporter accepts any record whose `sid` names a retained session. The
2617
+ * record may come from another reporter with the same session attributes,
2618
+ * or be hand-built. If the request has a record, a response that completes
2619
+ * after a `sid` change reports under its own retained session, with that
2620
+ * session's data snapshot and sequence numbers.
2207
2621
  *
2208
- * Request-time report keys come from the record's encoded per-call `data`
2209
- * snapshot, which the reporter decodes for each response. The record's `cid`
2210
- * replaces the session's current `cid` in the report. A response that
2211
- * completes after a mid-session content change therefore reports its
2212
- * request-time values.
2622
+ * With a record, request-time report keys come from the record's encoded
2623
+ * per-call `data` snapshot, which the reporter decodes for each response.
2624
+ * The record's `cid` replaces the session's current `cid` in the report. A
2625
+ * response that completes after a mid-session content change therefore
2626
+ * reports its request-time values.
2213
2627
  *
2214
2628
  * @typeParam RD - The `customData` of this request. Defaults to the
2215
2629
  * reporter's own `C`.
@@ -2218,21 +2632,19 @@ var CmcdReporter = class {
2218
2632
  * @param data - Additional CMCD data to include with the event.
2219
2633
  * Values provided here override the derived values.
2220
2634
  * The reporter ignores session-owned keys (`sid`, `msd`)
2221
- * supplied here. Attribution uses the provenance record alone.
2635
+ * supplied here.
2222
2636
  */
2223
2637
  recordResponseReceived(response, data = {}) {
2224
2638
  const { request } = response;
2225
2639
  const url = data.url ?? request?.url;
2226
2640
  if (!url) return;
2227
2641
  const provenance = request.customData?.[CMCD_REQUEST_PROVENANCE];
2228
- const session = this.resolveSession(provenance);
2642
+ const session = provenance === void 0 ? this.session : this.resolveSession(provenance);
2229
2643
  if (!session) return;
2230
- const cmcd = decodeSnapshot(provenance);
2231
- const { cid } = provenance;
2232
- const urlObj = new URL(url);
2233
- urlObj.searchParams.delete(CMCD_PARAM);
2644
+ const cmcd = provenance === void 0 ? { ...request.customData?.cmcd } : decodeSnapshot(provenance);
2645
+ const { cid } = provenance ?? {};
2234
2646
  const derived = {
2235
- url: urlObj.toString(),
2647
+ url: replaceCmcdParam(url),
2236
2648
  rc: response.status
2237
2649
  };
2238
2650
  const timing = response.resourceTiming;
@@ -2251,11 +2663,11 @@ var CmcdReporter = class {
2251
2663
  }, request);
2252
2664
  }
2253
2665
  /**
2254
- * Resolves the session a response belongs to. The provenance record's `sid`
2666
+ * Resolves the session that a provenance record names. The record's `sid`
2255
2667
  * must name one of this reporter's retained sessions, or the reporter
2256
- * discards the response. There is no other key. A lost record, or one that
2257
- * names a removed or unknown `sid`, resolves nothing. Any other attribution
2258
- * would be wrong. The reporter reads the `sid` as a plain property. A record
2668
+ * discards the response. A record that names a removed or unknown `sid`
2669
+ * resolves nothing. Any other attribution would be wrong. The reporter reads
2670
+ * the `sid` as a plain property. A record
2259
2671
  * copied through JSON therefore resolves the same session, and a hand-built
2260
2672
  * record that names a retained session is accepted.
2261
2673
  */
@@ -2359,17 +2771,13 @@ var CmcdReporter = class {
2359
2771
  const sendMsd = !isNaN(session.msd) && !session.requestTarget.msdSent;
2360
2772
  if (sendMsd) cmcdData.msd = session.msd;
2361
2773
  else delete cmcdData.msd;
2362
- const url = new URL(report.url);
2363
2774
  const options = createEncodingOptions(CMCD_REQUEST_MODE, this.config, report.url);
2364
2775
  const cmcd = report.customData.cmcd = copyReportValues(prepareCmcdData(cmcdData, options));
2365
2776
  if (sendMsd && cmcd.msd !== void 0) session.requestTarget.msdSent = true;
2366
2777
  const encoded = encodePreparedCmcd(cmcd);
2367
2778
  switch (this.config.transmissionMode) {
2368
2779
  case CMCD_QUERY:
2369
- if (encoded) {
2370
- url.searchParams.set(CMCD_PARAM, encoded);
2371
- report.url = url.toString();
2372
- }
2780
+ if (encoded) report.url = replaceCmcdParam(report.url, encoded);
2373
2781
  break;
2374
2782
  case CMCD_HEADERS:
2375
2783
  Object.assign(report.headers, toPreparedCmcdHeaders(cmcd, options.customHeaderMap));
@@ -2541,7 +2949,64 @@ function fromCmcdQuery(query, options) {
2541
2949
  //#endregion
2542
2950
  //#region src/fromCmcdUrl.ts
2543
2951
  function fromCmcdUrl(url, options) {
2544
- return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")), options);
2952
+ return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "").replace(/\+/g, " ")), options);
2953
+ }
2954
+
2955
+ //#endregion
2956
+ //#region src/isCmcdRequestKey.ts
2957
+ const CMCD_REQUEST_KEY_SET = /* @__PURE__ */ new Set(CMCD_REQUEST_KEYS);
2958
+ /**
2959
+ * Check if a key is a valid CMCD request key.
2960
+ *
2961
+ * @param key - The key to check.
2962
+ *
2963
+ * @returns `true` if the key is a valid CMCD request key, `false` otherwise.
2964
+ *
2965
+ * @public
2966
+ *
2967
+ * @example
2968
+ * {@includeCode ../test/isCmcdRequestKey.test.ts#example}
2969
+ */
2970
+ function isCmcdRequestKey(key) {
2971
+ return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
2972
+ }
2973
+
2974
+ //#endregion
2975
+ //#region src/isCmcdResponseReceivedKey.ts
2976
+ const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
2977
+ /**
2978
+ * Check if a key is a valid CMCD response key.
2979
+ *
2980
+ * @param key - The key to check.
2981
+ *
2982
+ * @returns `true` if the key is a valid CMCD response key, `false` otherwise.
2983
+ *
2984
+ * @public
2985
+ *
2986
+ * @example
2987
+ * {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
2988
+ */
2989
+ function isCmcdResponseReceivedKey(key) {
2990
+ return CMCD_RESPONSE_KEY_SET.has(key);
2991
+ }
2992
+
2993
+ //#endregion
2994
+ //#region src/isCmcdEventKey.ts
2995
+ const CMCD_EVENT_KEY_SET = /* @__PURE__ */ new Set(CMCD_EVENT_KEYS);
2996
+ /**
2997
+ * Check if a key is a valid CMCD event key.
2998
+ *
2999
+ * @param key - The key to check.
3000
+ *
3001
+ * @returns `true` if the key is a valid CMCD event key, `false` otherwise.
3002
+ *
3003
+ * @public
3004
+ *
3005
+ * @example
3006
+ * {@includeCode ../test/isCmcdEventKey.test.ts#example}
3007
+ */
3008
+ function isCmcdEventKey(key) {
3009
+ return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
2545
3010
  }
2546
3011
 
2547
3012
  //#endregion
@@ -2562,6 +3027,25 @@ function isCmcdV1Data(data) {
2562
3027
  return data.v !== CMCD_V2;
2563
3028
  }
2564
3029
 
3030
+ //#endregion
3031
+ //#region src/isCmcdV1Key.ts
3032
+ const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
3033
+ /**
3034
+ * Filter for CMCD version 1 keys.
3035
+ *
3036
+ * @param key - The CMCD key to filter.
3037
+ *
3038
+ * @returns `true` if the key should be included, `false` otherwise.
3039
+ *
3040
+ * @public
3041
+ *
3042
+ * @example
3043
+ * {@includeCode ../test/isCmcdV1Key.test.ts#example}
3044
+ */
3045
+ function isCmcdV1Key(key) {
3046
+ return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
3047
+ }
3048
+
2565
3049
  //#endregion
2566
3050
  //#region src/isCmcdV2Data.ts
2567
3051
  /**
@@ -2580,6 +3064,50 @@ function isCmcdV2Data(data) {
2580
3064
  return data.v === CMCD_V2;
2581
3065
  }
2582
3066
 
3067
+ //#endregion
3068
+ //#region src/toCmcdUrl.ts
3069
+ /**
3070
+ * Convert a CMCD object to a URL encoded string.
3071
+ *
3072
+ * Only letters, digits, `-`, `.`, `_`, and `~` stay unencoded, as in the
3073
+ * query examples of CTA-5004-B.
3074
+ *
3075
+ * @param cmcd - The CMCD object to convert.
3076
+ * @param options - Options for encoding.
3077
+ *
3078
+ * @returns The URL encoded CMCD data.
3079
+ *
3080
+ * @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}
3081
+ *
3082
+ * @public
3083
+ */
3084
+ function toCmcdUrl(cmcd, options = {}) {
3085
+ if (!cmcd) return "";
3086
+ return percentEncode(encodeCmcd(cmcd, options));
3087
+ }
3088
+
3089
+ //#endregion
3090
+ //#region src/toCmcdQuery.ts
3091
+ /**
3092
+ * Convert a CMCD object to a query arg.
3093
+ *
3094
+ * @param cmcd - The CMCD object to convert.
3095
+ * @param options - Options for encoding.
3096
+ *
3097
+ * @returns The CMCD query arg.
3098
+ *
3099
+ * @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}
3100
+ *
3101
+ * @public
3102
+ *
3103
+ * @example
3104
+ * {@includeCode ../test/toCmcdQuery.test.ts#example}
3105
+ */
3106
+ function toCmcdQuery(cmcd, options = {}) {
3107
+ if (!cmcd) return "";
3108
+ return `${CMCD_PARAM}=${toCmcdUrl(cmcd, options)}`;
3109
+ }
3110
+
2583
3111
  //#endregion
2584
3112
  //#region src/toCmcdValue.ts
2585
3113
  /**
@@ -2619,7 +3147,7 @@ function mergeValidationResults(...results) {
2619
3147
  */
2620
3148
  function resolveVersion(data, options) {
2621
3149
  if (options?.version === 1 || options?.version === 2) return options.version;
2622
- const payloadVersion = data["v"];
3150
+ const payloadVersion = toBareValue(data["v"]);
2623
3151
  if (payloadVersion === 1 || payloadVersion === 2) return payloadVersion;
2624
3152
  return CMCD_V1;
2625
3153
  }
@@ -2660,6 +3188,51 @@ function validateCmcdKeys(data, options) {
2660
3188
  };
2661
3189
  }
2662
3190
 
3191
+ //#endregion
3192
+ //#region src/CMCD_AGGREGATE_BITRATE_KEYS.ts
3193
+ /**
3194
+ * Maps each aggregate bitrate key to the exact bitrate key that supersedes it.
3195
+ *
3196
+ * Per CTA-5004-B, `ab` MUST NOT be sent when the encoded bitrate `br` is
3197
+ * known. `lab` and `tab` follow the same rule for the lowest bitrate `lb`
3198
+ * and the top bitrate `tb`.
3199
+ *
3200
+ * @internal
3201
+ */
3202
+ const CMCD_AGGREGATE_BITRATE_KEYS = {
3203
+ ab: "br",
3204
+ lab: "lb",
3205
+ tab: "tb"
3206
+ };
3207
+
3208
+ //#endregion
3209
+ //#region src/CMCD_KEY_OBJECT_TYPES.ts
3210
+ /**
3211
+ * Maps a key to the object types (`ot`) for which CTA-5004-B allows the key.
3212
+ *
3213
+ * A key that is absent from this map is allowed for every object type.
3214
+ * `d` (Object duration) and `tpb` (Top playable bitrate) MUST NOT be sent for
3215
+ * other object types.
3216
+ *
3217
+ * @internal
3218
+ */
3219
+ const CMCD_KEY_OBJECT_TYPES = {
3220
+ d: [
3221
+ "a",
3222
+ "v",
3223
+ "av",
3224
+ "tt",
3225
+ "c",
3226
+ "o"
3227
+ ],
3228
+ tpb: [
3229
+ "a",
3230
+ "v",
3231
+ "av",
3232
+ "c"
3233
+ ]
3234
+ };
3235
+
2663
3236
  //#endregion
2664
3237
  //#region src/validateCmcdStructure.ts
2665
3238
  /**
@@ -2727,9 +3300,9 @@ function validateCmcdStructure(data, options) {
2727
3300
  message: `Response key "${key}" must only be present when e="rr".`,
2728
3301
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2729
3302
  });
2730
- for (const [stateEventType, requiredField] of CMCD_STATE_EVENT_FIELDS) if (eventType === stateEventType && !(requiredField in data)) issues.push({
2731
- key: requiredField,
2732
- message: `State-change event (e="${stateEventType}") requires the "${requiredField}" key to be present.`,
3303
+ if (eventType === CMCD_EVENT_PLAY_STATE && !("sta" in data)) issues.push({
3304
+ key: "sta",
3305
+ message: "Play state change event (e=\"ps\") requires the \"sta\" key to be present.",
2733
3306
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2734
3307
  });
2735
3308
  if (eventType === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
@@ -2755,9 +3328,10 @@ function validateCmcdStructure(data, options) {
2755
3328
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2756
3329
  });
2757
3330
  }
2758
- if ("v" in data && data["v"] !== 1 && data["v"] !== 2) issues.push({
3331
+ const v = toBareValue(data["v"]);
3332
+ if ("v" in data && v !== 1 && v !== 2) issues.push({
2759
3333
  key: "v",
2760
- message: `Unsupported CMCD version "${String(data["v"])}". Expected 1 or 2.`,
3334
+ message: `Unsupported CMCD version "${String(v)}". Expected 1 or 2.`,
2761
3335
  severity: CMCD_VALIDATION_SEVERITY_ERROR
2762
3336
  });
2763
3337
  else if (version > 1 && !("v" in data)) issues.push({
@@ -2779,7 +3353,7 @@ function validateCmcdStructure(data, options) {
2779
3353
  //#endregion
2780
3354
  //#region src/CMCD_KEY_TYPES.ts
2781
3355
  /**
2782
- * CMCD key value type: inner list of numbers with token identifiers.
3356
+ * CMCD key value type: inner list of integers with token identifiers.
2783
3357
  *
2784
3358
  * @internal
2785
3359
  */
@@ -2886,9 +3460,9 @@ const CMCD_KEY_TYPES = {
2886
3460
  */
2887
3461
  const CMCD_V1_KEY_TYPE_OVERRIDES = {
2888
3462
  bl: CMCD_KEY_TYPE_INTEGER,
2889
- br: CMCD_KEY_TYPE_NUMBER,
3463
+ br: CMCD_KEY_TYPE_INTEGER,
2890
3464
  mtp: CMCD_KEY_TYPE_INTEGER,
2891
- tb: CMCD_KEY_TYPE_NUMBER,
3465
+ tb: CMCD_KEY_TYPE_INTEGER,
2892
3466
  nor: CMCD_KEY_TYPE_STRING
2893
3467
  };
2894
3468
 
@@ -2979,22 +3553,31 @@ const CMCD_TOKEN_VALUES = {
2979
3553
 
2980
3554
  //#endregion
2981
3555
  //#region src/validateCmcdValues.ts
2982
- const HUNDRED_ROUNDING_KEYS = /* @__PURE__ */ new Set([
2983
- "bl",
2984
- "dl",
2985
- "mtp",
2986
- "rtp",
2987
- "tbl"
2988
- ]);
2989
- const INTEGER_ROUNDING_KEYS = /* @__PURE__ */ new Set([
2990
- "br",
2991
- "d",
2992
- "tb"
2993
- ]);
3556
+ const HUNDRED_ROUNDING_UNITS = {
3557
+ bl: "ms",
3558
+ dl: "ms",
3559
+ mtp: "kbps",
3560
+ rtp: "kbps",
3561
+ tbl: "ms"
3562
+ };
2994
3563
  function isFiniteNumber(value) {
2995
3564
  return typeof value === "number" && Number.isFinite(value);
2996
3565
  }
2997
- function validateListValue(key, value, issues) {
3566
+ function isInteger(value) {
3567
+ return Number.isInteger(value);
3568
+ }
3569
+ function validateRounding(key, value, version, issues, index) {
3570
+ const unit = HUNDRED_ROUNDING_UNITS[key];
3571
+ if (!unit || value % 100 === 0) return;
3572
+ const should = key === "tbl" || key === "bl" && version !== CMCD_V1;
3573
+ const target = index === void 0 ? `Key "${key}"` : `Key "${key}" array element [${index}]`;
3574
+ issues.push({
3575
+ key,
3576
+ message: `${target} ${should ? "should" : "must"} be rounded to the nearest 100 ${unit}. Received ${value}.`,
3577
+ severity: should ? CMCD_VALIDATION_SEVERITY_WARNING : CMCD_VALIDATION_SEVERITY_ERROR
3578
+ });
3579
+ }
3580
+ function validateListValue(key, value, version, issues) {
2998
3581
  if (!Array.isArray(value)) {
2999
3582
  issues.push({
3000
3583
  key,
@@ -3004,18 +3587,13 @@ function validateListValue(key, value, issues) {
3004
3587
  return;
3005
3588
  }
3006
3589
  for (let i = 0; i < value.length; i++) {
3007
- const element = value[i];
3008
- if (element instanceof SfItem) {
3009
- if (!isFiniteNumber(element.value)) issues.push({
3010
- key,
3011
- message: `Key "${key}" array element [${i}] must be a finite number.`,
3012
- severity: CMCD_VALIDATION_SEVERITY_ERROR
3013
- });
3014
- } else if (!isFiniteNumber(element)) issues.push({
3590
+ const element = toBareValue(value[i]);
3591
+ if (!isInteger(element)) issues.push({
3015
3592
  key,
3016
- message: `Key "${key}" array element [${i}] must be a finite number.`,
3593
+ message: `Key "${key}" array element [${i}] must be a finite integer.`,
3017
3594
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3018
3595
  });
3596
+ else validateRounding(key, element, version, issues, i);
3019
3597
  }
3020
3598
  }
3021
3599
  function validateStringArrayValue(key, value, issues) {
@@ -3027,20 +3605,11 @@ function validateStringArrayValue(key, value, issues) {
3027
3605
  });
3028
3606
  return;
3029
3607
  }
3030
- for (let i = 0; i < value.length; i++) {
3031
- const element = value[i];
3032
- if (element instanceof SfItem) {
3033
- if (typeof element.value !== "string") issues.push({
3034
- key,
3035
- message: `Key "${key}" array element [${i}] must be a string.`,
3036
- severity: CMCD_VALIDATION_SEVERITY_ERROR
3037
- });
3038
- } else if (typeof element !== "string") issues.push({
3039
- key,
3040
- message: `Key "${key}" array element [${i}] must be a string.`,
3041
- severity: CMCD_VALIDATION_SEVERITY_ERROR
3042
- });
3043
- }
3608
+ for (let i = 0; i < value.length; i++) if (typeof toBareValue(value[i]) !== "string") issues.push({
3609
+ key,
3610
+ message: `Key "${key}" array element [${i}] must be a string.`,
3611
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
3612
+ });
3044
3613
  }
3045
3614
  /**
3046
3615
  * Validates that all values in a CMCD payload conform to the expected types and constraints.
@@ -3059,7 +3628,8 @@ function validateStringArrayValue(key, value, issues) {
3059
3628
  function validateCmcdValues(data, options) {
3060
3629
  const version = resolveVersion(data, options);
3061
3630
  const issues = [];
3062
- for (const [key, value] of Object.entries(data)) {
3631
+ for (const [key, entry] of Object.entries(data)) {
3632
+ const value = toBareValue(entry);
3063
3633
  if (isCmcdCustomKey(key)) {
3064
3634
  if (typeof value !== "string") issues.push({
3065
3635
  key,
@@ -3086,16 +3656,12 @@ function validateCmcdValues(data, options) {
3086
3656
  if (version === CMCD_V1 && key in CMCD_V1_KEY_TYPE_OVERRIDES) expectedType = CMCD_V1_KEY_TYPE_OVERRIDES[key];
3087
3657
  switch (expectedType) {
3088
3658
  case CMCD_KEY_TYPE_INTEGER:
3089
- if (!isFiniteNumber(value) || !Number.isInteger(value)) issues.push({
3659
+ if (!isInteger(value)) issues.push({
3090
3660
  key,
3091
3661
  message: `Key "${key}" must be a finite integer.`,
3092
3662
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3093
3663
  });
3094
- else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
3095
- key,
3096
- message: `Key "${key}" should be rounded to the nearest 100.`,
3097
- severity: CMCD_VALIDATION_SEVERITY_WARNING
3098
- });
3664
+ else validateRounding(key, value, version, issues);
3099
3665
  break;
3100
3666
  case CMCD_KEY_TYPE_NUMBER:
3101
3667
  if (!isFiniteNumber(value)) issues.push({
@@ -3103,16 +3669,6 @@ function validateCmcdValues(data, options) {
3103
3669
  message: `Key "${key}" must be a finite number.`,
3104
3670
  severity: CMCD_VALIDATION_SEVERITY_ERROR
3105
3671
  });
3106
- else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
3107
- key,
3108
- message: `Key "${key}" should be rounded to the nearest 100.`,
3109
- severity: CMCD_VALIDATION_SEVERITY_WARNING
3110
- });
3111
- else if (INTEGER_ROUNDING_KEYS.has(key) && !Number.isInteger(value)) issues.push({
3112
- key,
3113
- message: `Key "${key}" should be rounded to an integer.`,
3114
- severity: CMCD_VALIDATION_SEVERITY_WARNING
3115
- });
3116
3672
  break;
3117
3673
  case CMCD_KEY_TYPE_BOOLEAN:
3118
3674
  if (typeof value !== "boolean") issues.push({
@@ -3144,7 +3700,7 @@ function validateCmcdValues(data, options) {
3144
3700
  break;
3145
3701
  }
3146
3702
  case CMCD_KEY_TYPE_NUMBER_LIST:
3147
- validateListValue(key, value, issues);
3703
+ validateListValue(key, value, version, issues);
3148
3704
  break;
3149
3705
  case CMCD_KEY_TYPE_STRING_LIST:
3150
3706
  validateStringArrayValue(key, value, issues);