@svta/cml-cmcd 2.7.0 → 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/README.md +3 -3
- package/dist/index.d.ts +53 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1414 -931
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,98 +1,315 @@
|
|
|
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/
|
|
4
|
+
//#region src/CMCD_INNER_LIST_KEYS.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Inner-list keys in version 2 that are scalars in version 1.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* known. `lab` and `tab` follow the same rule for the lowest bitrate `lb`
|
|
10
|
-
* and the top bitrate `tb`.
|
|
8
|
+
* Used by both encoding (down-conversion) and decoding (up-conversion).
|
|
11
9
|
*
|
|
12
10
|
* @internal
|
|
13
11
|
*/
|
|
14
|
-
const
|
|
15
|
-
ab
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
};
|
|
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
|
+
]);
|
|
69
28
|
|
|
70
29
|
//#endregion
|
|
71
|
-
//#region src/
|
|
30
|
+
//#region src/CMCD_KEY_SPECS.ts
|
|
31
|
+
const MEDIA = [
|
|
32
|
+
"a",
|
|
33
|
+
"v",
|
|
34
|
+
"av",
|
|
35
|
+
"tt",
|
|
36
|
+
"c",
|
|
37
|
+
"o"
|
|
38
|
+
];
|
|
72
39
|
/**
|
|
73
|
-
*
|
|
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.
|
|
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`.
|
|
78
41
|
*
|
|
79
42
|
* @internal
|
|
80
43
|
*/
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|
|
96
313
|
};
|
|
97
314
|
|
|
98
315
|
//#endregion
|
|
@@ -105,998 +322,1122 @@ const CMCD_KEY_OBJECT_TYPES = {
|
|
|
105
322
|
const CMCD_V2 = 2;
|
|
106
323
|
|
|
107
324
|
//#endregion
|
|
108
|
-
//#region src/
|
|
325
|
+
//#region src/CmcdReportingMode.ts
|
|
109
326
|
/**
|
|
110
|
-
* CMCD event
|
|
327
|
+
* CMCD event mode variable name.
|
|
111
328
|
*
|
|
112
329
|
* @public
|
|
113
330
|
*/
|
|
114
|
-
const
|
|
331
|
+
const CMCD_EVENT_MODE = "event";
|
|
115
332
|
/**
|
|
116
|
-
* CMCD
|
|
333
|
+
* CMCD request mode variable name.
|
|
117
334
|
*
|
|
118
335
|
* @public
|
|
119
336
|
*/
|
|
120
|
-
const
|
|
337
|
+
const CMCD_REQUEST_MODE = "request";
|
|
121
338
|
/**
|
|
122
|
-
* CMCD
|
|
339
|
+
* CMCD reporting mode types.
|
|
123
340
|
*
|
|
124
|
-
* @
|
|
125
|
-
*/
|
|
126
|
-
const CMCD_EVENT_PLAYBACK_RATE = "pr";
|
|
127
|
-
/**
|
|
128
|
-
* CMCD event type 'e' (error).
|
|
341
|
+
* @enum
|
|
129
342
|
*
|
|
130
343
|
* @public
|
|
131
344
|
*/
|
|
132
|
-
const
|
|
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
|
+
}
|
|
133
356
|
/**
|
|
134
|
-
*
|
|
357
|
+
* Unwrap an inner list or SfItem value to a scalar.
|
|
135
358
|
*
|
|
136
|
-
*
|
|
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`.
|
|
137
362
|
*/
|
|
138
|
-
|
|
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
|
+
}
|
|
139
377
|
/**
|
|
140
|
-
* CMCD
|
|
378
|
+
* Down-convert version 2 CMCD data to version 1.
|
|
141
379
|
*
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
const CMCD_EVENT_CONTENT_ID = "c";
|
|
145
|
-
/**
|
|
146
|
-
* CMCD event type 'b' (backgrounded mode).
|
|
380
|
+
* - Extracts `nrr` from the `nor` SfItem's `r` parameter.
|
|
381
|
+
* - Unwraps inner-list values to scalars for the object type `ot`.
|
|
147
382
|
*
|
|
148
|
-
* @
|
|
383
|
+
* @internal
|
|
149
384
|
*/
|
|
150
|
-
|
|
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
|
+
}
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/isCmcdCustomKey.ts
|
|
407
|
+
const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
|
|
151
408
|
/**
|
|
152
|
-
*
|
|
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.
|
|
153
413
|
*
|
|
154
|
-
* @
|
|
155
|
-
*/
|
|
156
|
-
const CMCD_EVENT_MUTE = "m";
|
|
157
|
-
/**
|
|
158
|
-
* CMCD event type 'um' (unmute).
|
|
414
|
+
* @param key - The key to check.
|
|
159
415
|
*
|
|
160
|
-
* @
|
|
161
|
-
*/
|
|
162
|
-
const CMCD_EVENT_UNMUTE = "um";
|
|
163
|
-
/**
|
|
164
|
-
* CMCD event type 'pe' (player expand).
|
|
416
|
+
* @returns `true` if the key is a custom key, `false` otherwise.
|
|
165
417
|
*
|
|
166
418
|
* @public
|
|
167
|
-
*/
|
|
168
|
-
const CMCD_EVENT_PLAYER_EXPAND = "pe";
|
|
169
|
-
/**
|
|
170
|
-
* CMCD event type 'pc' (player collapse).
|
|
171
419
|
*
|
|
172
|
-
* @
|
|
420
|
+
* @example
|
|
421
|
+
* {@includeCode ../test/isCmcdCustomKey.test.ts#example}
|
|
173
422
|
*/
|
|
174
|
-
|
|
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
|
+
};
|
|
175
436
|
/**
|
|
176
|
-
*
|
|
437
|
+
* Returns the row of a key in the key table.
|
|
177
438
|
*
|
|
178
|
-
* @
|
|
179
|
-
*/
|
|
180
|
-
const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
|
|
181
|
-
/**
|
|
182
|
-
* CMCD event type 'as' (ad start).
|
|
439
|
+
* @param key - The key to look up.
|
|
183
440
|
*
|
|
184
|
-
* @
|
|
185
|
-
*/
|
|
186
|
-
const CMCD_EVENT_AD_START = "as";
|
|
187
|
-
/**
|
|
188
|
-
* CMCD event type 'ae' (ad end).
|
|
441
|
+
* @returns The row of a reserved key, the custom row for a valid custom key, or `undefined`.
|
|
189
442
|
*
|
|
190
|
-
* @
|
|
443
|
+
* @internal
|
|
191
444
|
*/
|
|
192
|
-
|
|
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;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/isValid.ts
|
|
193
452
|
/**
|
|
194
|
-
*
|
|
453
|
+
* Checks if the given value is valid to send as a CMCD value.
|
|
195
454
|
*
|
|
196
|
-
*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* CMCD event type 'abe' (ad break end).
|
|
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.
|
|
201
458
|
*
|
|
202
|
-
* @
|
|
203
|
-
*/
|
|
204
|
-
const CMCD_EVENT_AD_BREAK_END = "abe";
|
|
205
|
-
/**
|
|
206
|
-
* CMCD event type 'sk' (skip).
|
|
459
|
+
* @param value - The value to check.
|
|
207
460
|
*
|
|
208
|
-
* @
|
|
461
|
+
* @returns `true` if the value is valid.
|
|
462
|
+
*
|
|
463
|
+
* @internal
|
|
209
464
|
*/
|
|
210
|
-
|
|
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;
|
|
469
|
+
}
|
|
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
|
+
}
|
|
211
501
|
/**
|
|
212
|
-
*
|
|
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.
|
|
213
505
|
*
|
|
214
|
-
* @
|
|
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
|
|
215
512
|
*/
|
|
216
|
-
|
|
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
|
+
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/toTokenString.ts
|
|
217
527
|
/**
|
|
218
|
-
*
|
|
528
|
+
* Resolve the text of a token value.
|
|
219
529
|
*
|
|
220
|
-
*
|
|
530
|
+
* A token may be a plain string, a registry `Symbol`, an `SfToken`, or an
|
|
531
|
+
* `SfItem` that wraps such a value.
|
|
221
532
|
*
|
|
222
|
-
* @
|
|
533
|
+
* @param value - The value to resolve.
|
|
223
534
|
*
|
|
224
|
-
* @
|
|
535
|
+
* @returns The token text, or `undefined` when the value is not a token.
|
|
536
|
+
*
|
|
537
|
+
* @internal
|
|
225
538
|
*/
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
};
|
|
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);
|
|
543
|
+
}
|
|
246
544
|
|
|
247
545
|
//#endregion
|
|
248
|
-
//#region src/
|
|
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
|
+
}
|
|
249
591
|
/**
|
|
250
|
-
*
|
|
251
|
-
* the event signals.
|
|
592
|
+
* Applies the value rule of a key type from the key table.
|
|
252
593
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* (`prepareCmcdData`), deduplicate against its value (`CmcdReporter`), and
|
|
257
|
-
* check its presence in payloads (`validateCmcdStructure`).
|
|
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.
|
|
258
597
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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.
|
|
262
603
|
*
|
|
263
604
|
* @internal
|
|
264
605
|
*/
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
[CMCD_EVENT_BITRATE_CHANGE, "br"]
|
|
271
|
-
]);
|
|
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);
|
|
610
|
+
}
|
|
272
611
|
|
|
273
612
|
//#endregion
|
|
274
|
-
//#region src/
|
|
275
|
-
/**
|
|
276
|
-
* CMCD event mode variable name.
|
|
277
|
-
*
|
|
278
|
-
* @public
|
|
279
|
-
*/
|
|
280
|
-
const CMCD_EVENT_MODE = "event";
|
|
613
|
+
//#region src/toBareValue.ts
|
|
281
614
|
/**
|
|
282
|
-
*
|
|
615
|
+
* Returns the value inside an `SfItem`, or the value itself.
|
|
283
616
|
*
|
|
284
|
-
*
|
|
285
|
-
|
|
286
|
-
const CMCD_REQUEST_MODE = "request";
|
|
287
|
-
/**
|
|
288
|
-
* CMCD reporting mode types.
|
|
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.
|
|
289
619
|
*
|
|
290
|
-
* @
|
|
620
|
+
* @param value - The value to read.
|
|
621
|
+
* @returns The value without its parameters.
|
|
291
622
|
*
|
|
292
|
-
* @
|
|
623
|
+
* @internal
|
|
293
624
|
*/
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
};
|
|
625
|
+
function toBareValue(value) {
|
|
626
|
+
return value instanceof SfItem ? value.value : value;
|
|
627
|
+
}
|
|
298
628
|
|
|
299
629
|
//#endregion
|
|
300
|
-
//#region src/
|
|
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]);
|
|
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);
|
|
666
|
+
}
|
|
667
|
+
return formatted;
|
|
668
|
+
}
|
|
301
669
|
/**
|
|
302
|
-
*
|
|
670
|
+
* Convert a generic object to CMCD data.
|
|
671
|
+
*
|
|
672
|
+
* @param obj - The CMCD object to process.
|
|
673
|
+
* @param options - Options for encoding.
|
|
303
674
|
*
|
|
304
675
|
* @public
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* {@includeCode ../test/prepareCmcdData.test.ts#example}
|
|
305
679
|
*/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
|
|
311
|
-
|
|
680
|
+
function prepareCmcdData(obj, options = {}) {
|
|
681
|
+
const results = {};
|
|
682
|
+
if (obj == null || typeof obj !== "object") return results;
|
|
683
|
+
const version = options.version || toBareValue(obj["v"]) || CMCD_V2;
|
|
684
|
+
const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
|
|
685
|
+
const isV1 = version === 1;
|
|
686
|
+
const isEventReport = !isV1 && reportingMode === CMCD_EVENT_MODE;
|
|
687
|
+
const formatterOptions = {
|
|
688
|
+
version,
|
|
689
|
+
reportingMode,
|
|
690
|
+
baseUrl: options.baseUrl
|
|
691
|
+
};
|
|
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();
|
|
738
|
+
}
|
|
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;
|
|
743
|
+
}
|
|
744
|
+
return results;
|
|
745
|
+
}
|
|
312
746
|
|
|
313
747
|
//#endregion
|
|
314
|
-
//#region src/
|
|
748
|
+
//#region src/CmcdHeaderField.ts
|
|
315
749
|
/**
|
|
316
|
-
*
|
|
750
|
+
* CMCD object header name.
|
|
317
751
|
*
|
|
318
752
|
* @public
|
|
319
753
|
*/
|
|
320
|
-
const
|
|
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.-]*$/;
|
|
754
|
+
const CMCD_OBJECT = "CMCD-Object";
|
|
364
755
|
/**
|
|
365
|
-
*
|
|
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.
|
|
756
|
+
* CMCD request header name.
|
|
373
757
|
*
|
|
374
758
|
* @public
|
|
375
|
-
*
|
|
376
|
-
* @example
|
|
377
|
-
* {@includeCode ../test/isCmcdCustomKey.test.ts#example}
|
|
378
759
|
*/
|
|
379
|
-
|
|
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);
|
|
760
|
+
const CMCD_REQUEST = "CMCD-Request";
|
|
387
761
|
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* @param key - The key to check.
|
|
391
|
-
*
|
|
392
|
-
* @returns `true` if the key is a valid CMCD request key, `false` otherwise.
|
|
762
|
+
* CMCD session header name.
|
|
393
763
|
*
|
|
394
764
|
* @public
|
|
395
|
-
*
|
|
396
|
-
* @example
|
|
397
|
-
* {@includeCode ../test/isCmcdRequestKey.test.ts#example}
|
|
398
765
|
*/
|
|
399
|
-
|
|
400
|
-
return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
//#endregion
|
|
404
|
-
//#region src/CMCD_RESPONSE_KEYS.ts
|
|
766
|
+
const CMCD_SESSION = "CMCD-Session";
|
|
405
767
|
/**
|
|
406
|
-
*
|
|
768
|
+
* CMCD status header name.
|
|
407
769
|
*
|
|
408
770
|
* @public
|
|
409
771
|
*/
|
|
410
|
-
const
|
|
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);
|
|
772
|
+
const CMCD_STATUS = "CMCD-Status";
|
|
424
773
|
/**
|
|
425
|
-
*
|
|
774
|
+
* CMCD header fields.
|
|
426
775
|
*
|
|
427
|
-
* @param key - The key to check.
|
|
428
776
|
*
|
|
429
|
-
* @
|
|
777
|
+
* @enum
|
|
430
778
|
*
|
|
431
779
|
* @public
|
|
432
|
-
*
|
|
433
|
-
* @example
|
|
434
|
-
* {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
|
|
435
780
|
*/
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const CMCD_EVENT_KEY_SET = /* @__PURE__ */ new Set(CMCD_EVENT_KEYS);
|
|
781
|
+
const CmcdHeaderField = {
|
|
782
|
+
OBJECT: CMCD_OBJECT,
|
|
783
|
+
REQUEST: CMCD_REQUEST,
|
|
784
|
+
SESSION: CMCD_SESSION,
|
|
785
|
+
STATUS: CMCD_STATUS
|
|
786
|
+
};
|
|
443
787
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* @param key - The key to check.
|
|
447
|
-
*
|
|
448
|
-
* @returns `true` if the key is a valid CMCD event key, `false` otherwise.
|
|
788
|
+
* All CMCD header fields as an array.
|
|
449
789
|
*
|
|
450
790
|
* @public
|
|
451
|
-
*
|
|
452
|
-
* @example
|
|
453
|
-
* {@includeCode ../test/isCmcdEventKey.test.ts#example}
|
|
454
791
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
792
|
+
const CMCD_HEADER_FIELDS = [
|
|
793
|
+
CMCD_OBJECT,
|
|
794
|
+
CMCD_REQUEST,
|
|
795
|
+
CMCD_SESSION,
|
|
796
|
+
CMCD_STATUS
|
|
797
|
+
];
|
|
458
798
|
|
|
459
799
|
//#endregion
|
|
460
|
-
//#region src/
|
|
800
|
+
//#region src/CMCD_HEADER_MAP.ts
|
|
461
801
|
/**
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* Used by both encoding (down-conversion) and decoding (up-conversion).
|
|
802
|
+
* The map of CMCD keys to their appropriate header shard.
|
|
465
803
|
*
|
|
466
|
-
*
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
"ab",
|
|
470
|
-
"bl",
|
|
471
|
-
"br",
|
|
472
|
-
"bsa",
|
|
473
|
-
"bsd",
|
|
474
|
-
"bsda",
|
|
475
|
-
"lab",
|
|
476
|
-
"lb",
|
|
477
|
-
"mtp",
|
|
478
|
-
"pb",
|
|
479
|
-
"tab",
|
|
480
|
-
"tb",
|
|
481
|
-
"tbl",
|
|
482
|
-
"tpb"
|
|
483
|
-
]);
|
|
484
|
-
|
|
485
|
-
//#endregion
|
|
486
|
-
//#region src/CMCD_V1_KEYS.ts
|
|
487
|
-
/**
|
|
488
|
-
* Defines the keys for CMCD (Common Media Client Data) version 1.
|
|
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.
|
|
489
807
|
*
|
|
490
808
|
* @public
|
|
491
809
|
*/
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
810
|
+
const CMCD_HEADER_MAP = {
|
|
811
|
+
ab: CMCD_OBJECT,
|
|
812
|
+
br: CMCD_OBJECT,
|
|
813
|
+
d: CMCD_OBJECT,
|
|
814
|
+
lab: CMCD_OBJECT,
|
|
815
|
+
lb: CMCD_OBJECT,
|
|
816
|
+
ot: CMCD_OBJECT,
|
|
817
|
+
tab: CMCD_OBJECT,
|
|
818
|
+
tb: CMCD_OBJECT,
|
|
819
|
+
tpb: CMCD_OBJECT,
|
|
820
|
+
bl: CMCD_REQUEST,
|
|
821
|
+
cs: CMCD_REQUEST,
|
|
822
|
+
dfa: CMCD_REQUEST,
|
|
823
|
+
dl: CMCD_REQUEST,
|
|
824
|
+
ltc: CMCD_REQUEST,
|
|
825
|
+
mtp: CMCD_REQUEST,
|
|
826
|
+
nor: CMCD_REQUEST,
|
|
827
|
+
nrr: CMCD_REQUEST,
|
|
828
|
+
pb: CMCD_REQUEST,
|
|
829
|
+
sn: CMCD_REQUEST,
|
|
830
|
+
sta: CMCD_REQUEST,
|
|
831
|
+
su: CMCD_REQUEST,
|
|
832
|
+
tbl: CMCD_REQUEST,
|
|
833
|
+
cid: CMCD_SESSION,
|
|
834
|
+
msd: CMCD_SESSION,
|
|
835
|
+
sf: CMCD_SESSION,
|
|
836
|
+
sid: CMCD_SESSION,
|
|
837
|
+
st: CMCD_SESSION,
|
|
838
|
+
v: CMCD_SESSION,
|
|
839
|
+
bg: CMCD_STATUS,
|
|
840
|
+
bs: CMCD_STATUS,
|
|
841
|
+
bsa: CMCD_STATUS,
|
|
842
|
+
bsd: CMCD_STATUS,
|
|
843
|
+
bsda: CMCD_STATUS,
|
|
844
|
+
cdn: CMCD_STATUS,
|
|
845
|
+
ec: CMCD_STATUS,
|
|
846
|
+
nr: CMCD_STATUS,
|
|
847
|
+
pr: CMCD_STATUS,
|
|
848
|
+
pt: CMCD_STATUS,
|
|
849
|
+
rtp: CMCD_STATUS
|
|
850
|
+
};
|
|
512
851
|
|
|
513
852
|
//#endregion
|
|
514
|
-
//#region src/
|
|
515
|
-
|
|
853
|
+
//#region src/groupCmcdHeaders.ts
|
|
854
|
+
function createHeaderMap(headerMap) {
|
|
855
|
+
return Object.keys(headerMap).reduce((acc, field) => {
|
|
856
|
+
headerMap[field]?.forEach((key) => acc[key] = field);
|
|
857
|
+
return acc;
|
|
858
|
+
}, {});
|
|
859
|
+
}
|
|
516
860
|
/**
|
|
517
|
-
*
|
|
861
|
+
* Group a CMCD object into header shards
|
|
518
862
|
*
|
|
519
|
-
* @param
|
|
863
|
+
* @param cmcd - The CMCD object to convert.
|
|
864
|
+
* @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
|
|
520
865
|
*
|
|
521
|
-
* @returns
|
|
866
|
+
* @returns The CMCD header shards.
|
|
522
867
|
*
|
|
523
|
-
* @
|
|
868
|
+
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-B Header Field Definition}
|
|
524
869
|
*
|
|
525
|
-
* @
|
|
526
|
-
* {@includeCode ../test/isCmcdV1Key.test.ts#example}
|
|
870
|
+
* @public
|
|
527
871
|
*/
|
|
528
|
-
function
|
|
529
|
-
|
|
872
|
+
function groupCmcdHeaders(cmcd, customHeaderMap) {
|
|
873
|
+
const result = {};
|
|
874
|
+
if (!cmcd) return result;
|
|
875
|
+
const keys = Object.keys(cmcd);
|
|
876
|
+
const custom = customHeaderMap ? createHeaderMap(customHeaderMap) : {};
|
|
877
|
+
for (const key of keys) {
|
|
878
|
+
const field = CMCD_HEADER_MAP[key] || custom[key] || CmcdHeaderField.REQUEST;
|
|
879
|
+
const data = result[field] ??= {};
|
|
880
|
+
data[key] = cmcd[key];
|
|
881
|
+
}
|
|
882
|
+
return result;
|
|
530
883
|
}
|
|
531
884
|
|
|
532
885
|
//#endregion
|
|
533
|
-
//#region src/
|
|
534
|
-
const TOKEN_FIELDS = /* @__PURE__ */ new Set([
|
|
535
|
-
"ot",
|
|
536
|
-
"sf",
|
|
537
|
-
"st",
|
|
538
|
-
"e",
|
|
539
|
-
"sta"
|
|
540
|
-
]);
|
|
886
|
+
//#region src/toPreparedCmcdHeaders.ts
|
|
541
887
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* @param key - The key to check.
|
|
888
|
+
* Encode already-prepared CMCD data to CMCD header shards.
|
|
545
889
|
*
|
|
546
|
-
* @
|
|
890
|
+
* @param data - The prepared CMCD data to encode.
|
|
891
|
+
* @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
|
|
892
|
+
* @returns The CMCD header shards.
|
|
547
893
|
*
|
|
548
894
|
* @internal
|
|
549
895
|
*/
|
|
550
|
-
function
|
|
551
|
-
|
|
896
|
+
function toPreparedCmcdHeaders(data, customHeaderMap) {
|
|
897
|
+
const result = {};
|
|
898
|
+
const shards = groupCmcdHeaders(data, customHeaderMap);
|
|
899
|
+
for (const [field, value] of Object.entries(shards)) {
|
|
900
|
+
const shard = encodeSfDict(value, { whitespace: false });
|
|
901
|
+
if (shard) result[field] = shard;
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
552
904
|
}
|
|
553
905
|
|
|
554
906
|
//#endregion
|
|
555
|
-
//#region src/
|
|
907
|
+
//#region src/toCmcdHeaders.ts
|
|
556
908
|
/**
|
|
557
|
-
*
|
|
909
|
+
* Convert a CMCD object to request headers
|
|
558
910
|
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
* the key when its value is unknown.
|
|
911
|
+
* @param cmcd - The CMCD object to convert.
|
|
912
|
+
* @param options - Options for encoding.
|
|
562
913
|
*
|
|
563
|
-
* @
|
|
914
|
+
* @returns The CMCD header shards.
|
|
564
915
|
*
|
|
565
|
-
* @
|
|
916
|
+
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-B Header Field Definition}
|
|
566
917
|
*
|
|
567
|
-
* @
|
|
918
|
+
* @public
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* {@includeCode ../test/toCmcdHeaders.test.ts#example}
|
|
568
922
|
*/
|
|
569
|
-
function
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
return value != null && value !== "" && value !== false;
|
|
923
|
+
function toCmcdHeaders(cmcd, options = {}) {
|
|
924
|
+
if (!cmcd) return {};
|
|
925
|
+
return toPreparedCmcdHeaders(prepareCmcdData(cmcd, options), options?.customHeaderMap);
|
|
573
926
|
}
|
|
574
927
|
|
|
575
928
|
//#endregion
|
|
576
|
-
//#region src/
|
|
929
|
+
//#region src/appendCmcdHeaders.ts
|
|
577
930
|
/**
|
|
578
|
-
*
|
|
931
|
+
* Append CMCD header shards to a header object.
|
|
579
932
|
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
933
|
+
* @param headers - The headers to append to.
|
|
934
|
+
* @param cmcd - The CMCD object to append.
|
|
935
|
+
* @param options - Options for encoding.
|
|
582
936
|
*
|
|
583
|
-
* @
|
|
937
|
+
* @returns The headers with CMCD header shards appended.
|
|
584
938
|
*
|
|
585
|
-
* @
|
|
939
|
+
* @public
|
|
586
940
|
*
|
|
587
|
-
* @
|
|
941
|
+
* @example
|
|
942
|
+
* {@includeCode ../test/appendCmcdHeaders.test.ts#example}
|
|
943
|
+
*
|
|
944
|
+
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-B Header Field Definition}
|
|
588
945
|
*/
|
|
589
|
-
function
|
|
590
|
-
|
|
591
|
-
if (typeof value === "string") return value;
|
|
592
|
-
if (typeof value === "symbol" || value instanceof SfToken) return symbolToStr(value);
|
|
946
|
+
function appendCmcdHeaders(headers, cmcd, options) {
|
|
947
|
+
return Object.assign(headers, toCmcdHeaders(cmcd, options));
|
|
593
948
|
}
|
|
594
949
|
|
|
595
950
|
//#endregion
|
|
596
|
-
//#region src/
|
|
597
|
-
const filterMap = {
|
|
598
|
-
event: isCmcdEventKey,
|
|
599
|
-
request: isCmcdRequestKey
|
|
600
|
-
};
|
|
951
|
+
//#region src/CMCD_PARAM.ts
|
|
601
952
|
/**
|
|
602
|
-
*
|
|
953
|
+
* CMCD parameter name.
|
|
954
|
+
*
|
|
955
|
+
* @public
|
|
603
956
|
*/
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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;
|
|
614
|
-
}
|
|
957
|
+
const CMCD_PARAM = "CMCD";
|
|
958
|
+
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/encodePreparedCmcd.ts
|
|
615
961
|
/**
|
|
616
|
-
*
|
|
962
|
+
* Encode already-prepared CMCD data to a structured field dictionary string.
|
|
617
963
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
964
|
+
* @param data - The prepared CMCD data to encode.
|
|
965
|
+
* @returns The encoded CMCD string.
|
|
966
|
+
*
|
|
967
|
+
* @internal
|
|
620
968
|
*/
|
|
621
|
-
function
|
|
622
|
-
|
|
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;
|
|
638
|
-
}
|
|
639
|
-
function formatValue(key, value, options, formatterOptions) {
|
|
640
|
-
const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
|
|
641
|
-
return typeof formatter === "function" && isValid(value) ? formatter(value, formatterOptions) : value;
|
|
969
|
+
function encodePreparedCmcd(data) {
|
|
970
|
+
return encodeSfDict(data, { whitespace: false });
|
|
642
971
|
}
|
|
972
|
+
|
|
973
|
+
//#endregion
|
|
974
|
+
//#region src/encodeCmcd.ts
|
|
643
975
|
/**
|
|
644
|
-
*
|
|
976
|
+
* Encode a CMCD object to a string.
|
|
645
977
|
*
|
|
646
|
-
* @param
|
|
978
|
+
* @param cmcd - The CMCD object to encode.
|
|
647
979
|
* @param options - Options for encoding.
|
|
648
980
|
*
|
|
981
|
+
* @returns The encoded CMCD string.
|
|
982
|
+
*
|
|
983
|
+
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#payload-definition-for-headers-and-query-argument-transmission | CTA-5004-B Payload Definition}
|
|
984
|
+
*
|
|
649
985
|
* @public
|
|
650
986
|
*
|
|
651
987
|
* @example
|
|
652
|
-
* {@includeCode ../test/
|
|
988
|
+
* {@includeCode ../test/encodeCmcd.test.ts#example}
|
|
653
989
|
*/
|
|
654
|
-
function
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const version = options.version || obj["v"] || CMCD_V2;
|
|
658
|
-
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");
|
|
678
|
-
const formatterOptions = {
|
|
679
|
-
version,
|
|
680
|
-
reportingMode,
|
|
681
|
-
baseUrl: options.baseUrl
|
|
682
|
-
};
|
|
683
|
-
keys.sort();
|
|
684
|
-
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;
|
|
698
|
-
}
|
|
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);
|
|
710
|
-
}
|
|
711
|
-
results[key] = value;
|
|
712
|
-
}
|
|
713
|
-
return results;
|
|
990
|
+
function encodeCmcd(cmcd, options = {}) {
|
|
991
|
+
if (!cmcd) return "";
|
|
992
|
+
return encodePreparedCmcd(prepareCmcdData(cmcd, options));
|
|
714
993
|
}
|
|
715
994
|
|
|
716
995
|
//#endregion
|
|
717
|
-
//#region src/
|
|
996
|
+
//#region src/toCmcdUrl.ts
|
|
718
997
|
/**
|
|
719
|
-
* CMCD object
|
|
998
|
+
* Convert a CMCD object to a URL encoded string.
|
|
999
|
+
*
|
|
1000
|
+
* @param cmcd - The CMCD object to convert.
|
|
1001
|
+
* @param options - Options for encoding.
|
|
1002
|
+
*
|
|
1003
|
+
* @returns The URL encoded CMCD data.
|
|
1004
|
+
*
|
|
1005
|
+
* @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}
|
|
720
1006
|
*
|
|
721
1007
|
* @public
|
|
722
1008
|
*/
|
|
723
|
-
|
|
1009
|
+
function toCmcdUrl(cmcd, options = {}) {
|
|
1010
|
+
if (!cmcd) return "";
|
|
1011
|
+
const params = encodeCmcd(cmcd, options);
|
|
1012
|
+
return encodeURIComponent(params);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/toCmcdQuery.ts
|
|
724
1017
|
/**
|
|
725
|
-
* CMCD
|
|
1018
|
+
* Convert a CMCD object to a query arg.
|
|
1019
|
+
*
|
|
1020
|
+
* @param cmcd - The CMCD object to convert.
|
|
1021
|
+
* @param options - Options for encoding.
|
|
1022
|
+
*
|
|
1023
|
+
* @returns The CMCD query arg.
|
|
1024
|
+
*
|
|
1025
|
+
* @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}
|
|
726
1026
|
*
|
|
727
1027
|
* @public
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* {@includeCode ../test/toCmcdQuery.test.ts#example}
|
|
728
1031
|
*/
|
|
729
|
-
|
|
1032
|
+
function toCmcdQuery(cmcd, options = {}) {
|
|
1033
|
+
if (!cmcd) return "";
|
|
1034
|
+
return `${CMCD_PARAM}=${toCmcdUrl(cmcd, options)}`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/appendCmcdQuery.ts
|
|
1039
|
+
const REGEX = /CMCD=[^&#]+/;
|
|
730
1040
|
/**
|
|
731
|
-
* CMCD
|
|
1041
|
+
* Append CMCD query args to a URL.
|
|
1042
|
+
*
|
|
1043
|
+
* @param url - The URL to append to.
|
|
1044
|
+
* @param cmcd - The CMCD object to append.
|
|
1045
|
+
* @param options - Options for encoding.
|
|
1046
|
+
*
|
|
1047
|
+
* @returns The URL with the CMCD query args appended.
|
|
732
1048
|
*
|
|
733
1049
|
* @public
|
|
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}
|
|
734
1055
|
*/
|
|
735
|
-
|
|
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
|
|
736
1065
|
/**
|
|
737
|
-
*
|
|
1066
|
+
* The default time interval in seconds when using event mode
|
|
738
1067
|
*
|
|
739
1068
|
* @public
|
|
740
1069
|
*/
|
|
741
|
-
const
|
|
1070
|
+
const CMCD_DEFAULT_TIME_INTERVAL = 30;
|
|
1071
|
+
|
|
1072
|
+
//#endregion
|
|
1073
|
+
//#region src/CMCD_EVENT_KEYS.ts
|
|
742
1074
|
/**
|
|
743
|
-
* CMCD
|
|
1075
|
+
* Defines the event-specific keys for CMCD (Common Media Client Data) version 2.
|
|
744
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.
|
|
745
1109
|
*
|
|
746
|
-
*
|
|
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.
|
|
747
1113
|
*
|
|
748
1114
|
* @public
|
|
749
1115
|
*/
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
|
755
1146
|
};
|
|
1147
|
+
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/CMCD_REQUEST_KEYS.ts
|
|
756
1150
|
/**
|
|
757
|
-
*
|
|
1151
|
+
* Defines the request-specific keys for CMCD (Common Media Client Data) version 2.
|
|
758
1152
|
*
|
|
759
1153
|
* @public
|
|
760
1154
|
*/
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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"
|
|
766
1194
|
];
|
|
767
1195
|
|
|
768
1196
|
//#endregion
|
|
769
|
-
//#region src/
|
|
1197
|
+
//#region src/CMCD_RESPONSE_KEYS.ts
|
|
770
1198
|
/**
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
* Event-only keys (e, ts, cen, h) and response-received keys (rc, ttfb,
|
|
774
|
-
* ttlb, url, and others) are intentionally absent. They are transmitted
|
|
775
|
-
* in the event-mode POST body, not HTTP headers.
|
|
1199
|
+
* Response-only and timing keys for version 2.
|
|
776
1200
|
*
|
|
777
1201
|
* @public
|
|
778
1202
|
*/
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
bl: CMCD_REQUEST,
|
|
790
|
-
cs: CMCD_REQUEST,
|
|
791
|
-
dfa: CMCD_REQUEST,
|
|
792
|
-
dl: CMCD_REQUEST,
|
|
793
|
-
ltc: CMCD_REQUEST,
|
|
794
|
-
mtp: CMCD_REQUEST,
|
|
795
|
-
nor: CMCD_REQUEST,
|
|
796
|
-
nrr: CMCD_REQUEST,
|
|
797
|
-
pb: CMCD_REQUEST,
|
|
798
|
-
sn: CMCD_REQUEST,
|
|
799
|
-
sta: CMCD_REQUEST,
|
|
800
|
-
su: CMCD_REQUEST,
|
|
801
|
-
tbl: CMCD_REQUEST,
|
|
802
|
-
cid: CMCD_SESSION,
|
|
803
|
-
msd: CMCD_SESSION,
|
|
804
|
-
sf: CMCD_SESSION,
|
|
805
|
-
sid: CMCD_SESSION,
|
|
806
|
-
st: CMCD_SESSION,
|
|
807
|
-
v: CMCD_SESSION,
|
|
808
|
-
bg: CMCD_STATUS,
|
|
809
|
-
bs: CMCD_STATUS,
|
|
810
|
-
bsa: CMCD_STATUS,
|
|
811
|
-
bsd: CMCD_STATUS,
|
|
812
|
-
bsda: CMCD_STATUS,
|
|
813
|
-
cdn: CMCD_STATUS,
|
|
814
|
-
ec: CMCD_STATUS,
|
|
815
|
-
nr: CMCD_STATUS,
|
|
816
|
-
pr: CMCD_STATUS,
|
|
817
|
-
pt: CMCD_STATUS,
|
|
818
|
-
rtp: CMCD_STATUS
|
|
819
|
-
};
|
|
1203
|
+
const CMCD_RESPONSE_KEYS = [
|
|
1204
|
+
"cmsdd",
|
|
1205
|
+
"cmsds",
|
|
1206
|
+
"rc",
|
|
1207
|
+
"smrt",
|
|
1208
|
+
"ttfb",
|
|
1209
|
+
"ttfbb",
|
|
1210
|
+
"ttlb",
|
|
1211
|
+
"url"
|
|
1212
|
+
];
|
|
820
1213
|
|
|
821
1214
|
//#endregion
|
|
822
|
-
//#region src/
|
|
823
|
-
function createHeaderMap(headerMap) {
|
|
824
|
-
return Object.keys(headerMap).reduce((acc, field) => {
|
|
825
|
-
headerMap[field]?.forEach((key) => acc[key] = field);
|
|
826
|
-
return acc;
|
|
827
|
-
}, {});
|
|
828
|
-
}
|
|
1215
|
+
//#region src/CMCD_V1_KEYS.ts
|
|
829
1216
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
* @param cmcd - The CMCD object to convert.
|
|
833
|
-
* @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
|
|
834
|
-
*
|
|
835
|
-
* @returns The CMCD header shards.
|
|
836
|
-
*
|
|
837
|
-
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-B Header Field Definition}
|
|
1217
|
+
* Defines the keys for CMCD (Common Media Client Data) version 1.
|
|
838
1218
|
*
|
|
839
1219
|
* @public
|
|
840
1220
|
*/
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
+
];
|
|
853
1241
|
|
|
854
1242
|
//#endregion
|
|
855
|
-
//#region src/
|
|
1243
|
+
//#region src/CMCD_KEYS.ts
|
|
856
1244
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
* @param data - The prepared CMCD data to encode.
|
|
860
|
-
* @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
|
|
861
|
-
* @returns The CMCD header shards.
|
|
1245
|
+
* A list of all CMCD keys.
|
|
862
1246
|
*
|
|
863
|
-
* @
|
|
1247
|
+
* @public
|
|
864
1248
|
*/
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
return result;
|
|
873
|
-
}
|
|
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);
|
|
874
1255
|
|
|
875
1256
|
//#endregion
|
|
876
|
-
//#region src/
|
|
1257
|
+
//#region src/CMCD_MIME_TYPE.ts
|
|
877
1258
|
/**
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
* @param cmcd - The CMCD object to convert.
|
|
881
|
-
* @param options - Options for encoding.
|
|
882
|
-
*
|
|
883
|
-
* @returns The CMCD header shards.
|
|
884
|
-
*
|
|
885
|
-
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-B Header Field Definition}
|
|
1259
|
+
* CMCD MIME type for event report payloads.
|
|
886
1260
|
*
|
|
887
1261
|
* @public
|
|
888
|
-
*
|
|
889
|
-
* @example
|
|
890
|
-
* {@includeCode ../test/toCmcdHeaders.test.ts#example}
|
|
891
1262
|
*/
|
|
892
|
-
|
|
893
|
-
if (!cmcd) return {};
|
|
894
|
-
return toPreparedCmcdHeaders(prepareCmcdData(cmcd, options), options?.customHeaderMap);
|
|
895
|
-
}
|
|
1263
|
+
const CMCD_MIME_TYPE = "application/cmcd";
|
|
896
1264
|
|
|
897
1265
|
//#endregion
|
|
898
|
-
//#region src/
|
|
1266
|
+
//#region src/CMCD_REQUEST_PROVENANCE.ts
|
|
899
1267
|
/**
|
|
900
|
-
*
|
|
1268
|
+
* The `customData` property where `CmcdReporter.createRequestReport()`
|
|
1269
|
+
* writes session provenance on every request it returns.
|
|
901
1270
|
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
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.
|
|
905
1284
|
*
|
|
906
|
-
*
|
|
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.
|
|
907
1288
|
*
|
|
908
1289
|
* @public
|
|
909
|
-
*
|
|
910
|
-
* @example
|
|
911
|
-
* {@includeCode ../test/appendCmcdHeaders.test.ts#example}
|
|
912
|
-
*
|
|
913
|
-
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-B Header Field Definition}
|
|
914
1290
|
*/
|
|
915
|
-
|
|
916
|
-
return Object.assign(headers, toCmcdHeaders(cmcd, options));
|
|
917
|
-
}
|
|
1291
|
+
const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
|
|
918
1292
|
|
|
919
1293
|
//#endregion
|
|
920
|
-
//#region src/
|
|
1294
|
+
//#region src/CMCD_V1.ts
|
|
921
1295
|
/**
|
|
922
|
-
* CMCD
|
|
1296
|
+
* CMCD Version 1
|
|
923
1297
|
*
|
|
924
1298
|
* @public
|
|
925
1299
|
*/
|
|
926
|
-
const
|
|
1300
|
+
const CMCD_V1 = 1;
|
|
927
1301
|
|
|
928
1302
|
//#endregion
|
|
929
|
-
//#region src/
|
|
1303
|
+
//#region src/CmcdEventType.ts
|
|
930
1304
|
/**
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
* @param data - The prepared CMCD data to encode.
|
|
934
|
-
* @returns The encoded CMCD string.
|
|
1305
|
+
* CMCD event type 'bc' (bitrate change).
|
|
935
1306
|
*
|
|
936
|
-
* @
|
|
1307
|
+
* @public
|
|
937
1308
|
*/
|
|
938
|
-
|
|
939
|
-
return encodeSfDict(data, { whitespace: false });
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
//#endregion
|
|
943
|
-
//#region src/encodeCmcd.ts
|
|
1309
|
+
const CMCD_EVENT_BITRATE_CHANGE = "bc";
|
|
944
1310
|
/**
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
* @param cmcd - The CMCD object to encode.
|
|
948
|
-
* @param options - Options for encoding.
|
|
949
|
-
*
|
|
950
|
-
* @returns The encoded CMCD string.
|
|
951
|
-
*
|
|
952
|
-
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#payload-definition-for-headers-and-query-argument-transmission | CTA-5004-B Payload Definition}
|
|
1311
|
+
* CMCD event type 'ps' (play state change).
|
|
953
1312
|
*
|
|
954
1313
|
* @public
|
|
955
|
-
*
|
|
956
|
-
* @example
|
|
957
|
-
* {@includeCode ../test/encodeCmcd.test.ts#example}
|
|
958
1314
|
*/
|
|
959
|
-
|
|
960
|
-
if (!cmcd) return "";
|
|
961
|
-
return encodePreparedCmcd(prepareCmcdData(cmcd, options));
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
//#endregion
|
|
965
|
-
//#region src/toCmcdUrl.ts
|
|
1315
|
+
const CMCD_EVENT_PLAY_STATE = "ps";
|
|
966
1316
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
* @param cmcd - The CMCD object to convert.
|
|
970
|
-
* @param options - Options for encoding.
|
|
971
|
-
*
|
|
972
|
-
* @returns The URL encoded CMCD data.
|
|
973
|
-
*
|
|
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}
|
|
1317
|
+
* CMCD event type 'pr' (playback rate change).
|
|
975
1318
|
*
|
|
976
1319
|
* @public
|
|
977
1320
|
*/
|
|
978
|
-
|
|
979
|
-
if (!cmcd) return "";
|
|
980
|
-
const params = encodeCmcd(cmcd, options);
|
|
981
|
-
return encodeURIComponent(params);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
//#endregion
|
|
985
|
-
//#region src/toCmcdQuery.ts
|
|
1321
|
+
const CMCD_EVENT_PLAYBACK_RATE = "pr";
|
|
986
1322
|
/**
|
|
987
|
-
*
|
|
1323
|
+
* CMCD event type 'e' (error).
|
|
988
1324
|
*
|
|
989
|
-
* @
|
|
990
|
-
|
|
1325
|
+
* @public
|
|
1326
|
+
*/
|
|
1327
|
+
const CMCD_EVENT_ERROR = "e";
|
|
1328
|
+
/**
|
|
1329
|
+
* CMCD event type 't' (time interval).
|
|
991
1330
|
*
|
|
992
|
-
* @
|
|
1331
|
+
* @public
|
|
1332
|
+
*/
|
|
1333
|
+
const CMCD_EVENT_TIME_INTERVAL = "t";
|
|
1334
|
+
/**
|
|
1335
|
+
* CMCD event type 'c' (content ID).
|
|
993
1336
|
*
|
|
994
|
-
* @
|
|
1337
|
+
* @public
|
|
1338
|
+
*/
|
|
1339
|
+
const CMCD_EVENT_CONTENT_ID = "c";
|
|
1340
|
+
/**
|
|
1341
|
+
* CMCD event type 'b' (backgrounded mode).
|
|
995
1342
|
*
|
|
996
1343
|
* @public
|
|
1344
|
+
*/
|
|
1345
|
+
const CMCD_EVENT_BACKGROUNDED_MODE = "b";
|
|
1346
|
+
/**
|
|
1347
|
+
* CMCD event type 'm' (mute).
|
|
997
1348
|
*
|
|
998
|
-
* @
|
|
999
|
-
* {@includeCode ../test/toCmcdQuery.test.ts#example}
|
|
1349
|
+
* @public
|
|
1000
1350
|
*/
|
|
1001
|
-
|
|
1002
|
-
if (!cmcd) return "";
|
|
1003
|
-
return `${CMCD_PARAM}=${toCmcdUrl(cmcd, options)}`;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
//#endregion
|
|
1007
|
-
//#region src/appendCmcdQuery.ts
|
|
1008
|
-
const REGEX = /CMCD=[^&#]+/;
|
|
1351
|
+
const CMCD_EVENT_MUTE = "m";
|
|
1009
1352
|
/**
|
|
1010
|
-
*
|
|
1353
|
+
* CMCD event type 'um' (unmute).
|
|
1011
1354
|
*
|
|
1012
|
-
* @
|
|
1013
|
-
|
|
1014
|
-
|
|
1355
|
+
* @public
|
|
1356
|
+
*/
|
|
1357
|
+
const CMCD_EVENT_UNMUTE = "um";
|
|
1358
|
+
/**
|
|
1359
|
+
* CMCD event type 'pe' (player expand).
|
|
1015
1360
|
*
|
|
1016
|
-
* @
|
|
1361
|
+
* @public
|
|
1362
|
+
*/
|
|
1363
|
+
const CMCD_EVENT_PLAYER_EXPAND = "pe";
|
|
1364
|
+
/**
|
|
1365
|
+
* CMCD event type 'pc' (player collapse).
|
|
1017
1366
|
*
|
|
1018
1367
|
* @public
|
|
1368
|
+
*/
|
|
1369
|
+
const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
|
|
1370
|
+
/**
|
|
1371
|
+
* CMCD event type 'rr' (response received).
|
|
1019
1372
|
*
|
|
1020
|
-
* @
|
|
1021
|
-
|
|
1373
|
+
* @public
|
|
1374
|
+
*/
|
|
1375
|
+
const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
|
|
1376
|
+
/**
|
|
1377
|
+
* CMCD event type 'as' (ad start).
|
|
1022
1378
|
*
|
|
1023
|
-
* @
|
|
1379
|
+
* @public
|
|
1024
1380
|
*/
|
|
1025
|
-
|
|
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}`;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
//#endregion
|
|
1033
|
-
//#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
|
|
1381
|
+
const CMCD_EVENT_AD_START = "as";
|
|
1034
1382
|
/**
|
|
1035
|
-
*
|
|
1383
|
+
* CMCD event type 'ae' (ad end).
|
|
1036
1384
|
*
|
|
1037
1385
|
* @public
|
|
1038
1386
|
*/
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
//#endregion
|
|
1042
|
-
//#region src/CMCD_KEYS.ts
|
|
1387
|
+
const CMCD_EVENT_AD_END = "ae";
|
|
1043
1388
|
/**
|
|
1044
|
-
*
|
|
1389
|
+
* CMCD event type 'abs' (ad break start).
|
|
1045
1390
|
*
|
|
1046
1391
|
* @public
|
|
1047
1392
|
*/
|
|
1048
|
-
const
|
|
1049
|
-
...CMCD_V1_KEYS,
|
|
1050
|
-
...CMCD_REQUEST_KEYS,
|
|
1051
|
-
...CMCD_RESPONSE_KEYS,
|
|
1052
|
-
...CMCD_EVENT_KEYS
|
|
1053
|
-
].filter((key, index, arr) => arr.indexOf(key) === index);
|
|
1054
|
-
|
|
1055
|
-
//#endregion
|
|
1056
|
-
//#region src/CMCD_MIME_TYPE.ts
|
|
1393
|
+
const CMCD_EVENT_AD_BREAK_START = "abs";
|
|
1057
1394
|
/**
|
|
1058
|
-
* CMCD
|
|
1395
|
+
* CMCD event type 'abe' (ad break end).
|
|
1059
1396
|
*
|
|
1060
1397
|
* @public
|
|
1061
1398
|
*/
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
//#endregion
|
|
1065
|
-
//#region src/CMCD_REQUEST_PROVENANCE.ts
|
|
1399
|
+
const CMCD_EVENT_AD_BREAK_END = "abe";
|
|
1066
1400
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
* writes session provenance on every request it returns.
|
|
1069
|
-
*
|
|
1070
|
-
* The value is a frozen `CmcdRequestProvenance` record. `sid` names the
|
|
1071
|
-
* session that issued the request. `cid` is the content id at the time the
|
|
1072
|
-
* request was issued. `data` is the request's per-call data as an encoded
|
|
1073
|
-
* CMCD string. `CmcdReporter.recordResponseReceived()` reads the record to
|
|
1074
|
-
* attribute a late response to its session and to rebuild the request-time
|
|
1075
|
-
* report keys.
|
|
1076
|
-
* Spread and `Object.assign` copy the record into ordinary request clones.
|
|
1077
|
-
* `JSON.stringify` and structured clone drop symbol-keyed properties. Before
|
|
1078
|
-
* either operation, read the value and restore it afterward. JSON preserves
|
|
1079
|
-
* the record's fields. Attribution uses the `sid` value rather than object
|
|
1080
|
-
* identity, so a parsed plain-object copy attributes correctly. For a record
|
|
1081
|
-
* naming a `sid` the reporter does not retain, the reporter drops the
|
|
1082
|
-
* response rather than reattributing it.
|
|
1401
|
+
* CMCD event type 'sk' (skip).
|
|
1083
1402
|
*
|
|
1084
|
-
*
|
|
1085
|
-
|
|
1086
|
-
|
|
1403
|
+
* @public
|
|
1404
|
+
*/
|
|
1405
|
+
const CMCD_EVENT_SKIP = "sk";
|
|
1406
|
+
/**
|
|
1407
|
+
* CMCD event type 'ce' (custom event).
|
|
1087
1408
|
*
|
|
1088
1409
|
* @public
|
|
1089
1410
|
*/
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
//#endregion
|
|
1093
|
-
//#region src/CMCD_V1.ts
|
|
1411
|
+
const CMCD_EVENT_CUSTOM_EVENT = "ce";
|
|
1094
1412
|
/**
|
|
1095
|
-
* CMCD
|
|
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}
|
|
1096
1418
|
*
|
|
1097
1419
|
* @public
|
|
1098
1420
|
*/
|
|
1099
|
-
const
|
|
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
|
+
};
|
|
1100
1441
|
|
|
1101
1442
|
//#endregion
|
|
1102
1443
|
//#region src/CmcdObjectType.ts
|
|
@@ -1556,6 +1897,39 @@ var CmcdReportRecorder = class {
|
|
|
1556
1897
|
}
|
|
1557
1898
|
};
|
|
1558
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
|
+
|
|
1559
1933
|
//#endregion
|
|
1560
1934
|
//#region src/CmcdTransmissionMode.ts
|
|
1561
1935
|
/**
|
|
@@ -1599,19 +1973,21 @@ const CmcdTransmissionMode = {
|
|
|
1599
1973
|
* - Wraps scalar values in arrays for inner-list keys.
|
|
1600
1974
|
* - Wraps `nor` string in an array.
|
|
1601
1975
|
*
|
|
1602
|
-
* The function returns version 2 data (has `v: 2`) 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.
|
|
1603
1979
|
*
|
|
1604
1980
|
* @internal
|
|
1605
1981
|
*/
|
|
1606
1982
|
function upConvertToV2(obj) {
|
|
1607
|
-
if (obj["v"] === CMCD_V2) return obj;
|
|
1983
|
+
if (toBareValue(obj["v"]) === CMCD_V2) return obj;
|
|
1608
1984
|
const result = {};
|
|
1609
1985
|
for (const [key, value] of Object.entries(obj)) {
|
|
1610
1986
|
if (value == null) {
|
|
1611
1987
|
result[key] = value;
|
|
1612
1988
|
continue;
|
|
1613
1989
|
}
|
|
1614
|
-
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];
|
|
1615
1991
|
else if (key === "nor" && typeof value === "string") result[key] = [value];
|
|
1616
1992
|
else result[key] = value;
|
|
1617
1993
|
}
|
|
@@ -1661,10 +2037,9 @@ function cmcdObjectTypeListEqual(a, b) {
|
|
|
1661
2037
|
const ai = a[i];
|
|
1662
2038
|
const bi = b[i];
|
|
1663
2039
|
if (ai === bi) continue;
|
|
1664
|
-
if (typeof ai === "number"
|
|
1665
|
-
|
|
1666
|
-
const
|
|
1667
|
-
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;
|
|
1668
2043
|
const ak = ap && Object.keys(ap)[0];
|
|
1669
2044
|
const bk = bp && Object.keys(bp)[0];
|
|
1670
2045
|
if (ak !== bk) return false;
|
|
@@ -1672,6 +2047,18 @@ function cmcdObjectTypeListEqual(a, b) {
|
|
|
1672
2047
|
}
|
|
1673
2048
|
return true;
|
|
1674
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
|
+
}
|
|
1675
2062
|
const equal = Object.is;
|
|
1676
2063
|
const identity = (v) => v;
|
|
1677
2064
|
/**
|
|
@@ -1682,8 +2069,8 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
|
|
|
1682
2069
|
if (field === "br") return {
|
|
1683
2070
|
event,
|
|
1684
2071
|
field,
|
|
1685
|
-
equal:
|
|
1686
|
-
snapshot: (v) => v.slice()
|
|
2072
|
+
equal: brEqual,
|
|
2073
|
+
snapshot: (v) => Array.isArray(v) ? v.slice() : copyItemValue(v)
|
|
1687
2074
|
};
|
|
1688
2075
|
return {
|
|
1689
2076
|
event,
|
|
@@ -1702,29 +2089,14 @@ function buildRequiredEventKeys() {
|
|
|
1702
2089
|
]);
|
|
1703
2090
|
}
|
|
1704
2091
|
/**
|
|
1705
|
-
* Maps each event type to the key
|
|
1706
|
-
* Built from the state-change table plus the three
|
|
1707
|
-
* required key is event data, not player state.
|
|
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.
|
|
1708
2095
|
*/
|
|
1709
2096
|
const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ buildRequiredEventKeys();
|
|
1710
2097
|
/**
|
|
1711
|
-
*
|
|
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.
|
|
2098
|
+
* Copies a value with the `SfItem` structure, including its `params` record
|
|
2099
|
+
* and the list of an `SfItem` that wraps a list.
|
|
1728
2100
|
*
|
|
1729
2101
|
* The copy keeps the prototype because `prepareCmcdData`, the formatter map,
|
|
1730
2102
|
* validation, and the structured-field encoder all branch on
|
|
@@ -1735,6 +2107,7 @@ function copyItemValue(value) {
|
|
|
1735
2107
|
if (value === null || typeof value !== "object") return value;
|
|
1736
2108
|
const copy = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
|
|
1737
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);
|
|
1738
2111
|
return copy;
|
|
1739
2112
|
}
|
|
1740
2113
|
/**
|
|
@@ -1744,7 +2117,8 @@ function copyItemValue(value) {
|
|
|
1744
2117
|
*
|
|
1745
2118
|
* The copy is complete for the CMCD value space. `CmcdValue` and
|
|
1746
2119
|
* `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
|
|
1747
|
-
* those. `
|
|
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
|
|
1748
2122
|
* where a transform is configured, at session end on the ended session's
|
|
1749
2123
|
* store, and on the request's stored player-facing view. The session-end
|
|
1750
2124
|
* copy detaches the frozen snapshot from caller-held references.
|
|
@@ -1873,7 +2247,7 @@ var CmcdReporter = class {
|
|
|
1873
2247
|
provenance: mintProvenance(sid, data.cid),
|
|
1874
2248
|
data,
|
|
1875
2249
|
msd: NaN,
|
|
1876
|
-
lastEmitted: {},
|
|
2250
|
+
lastEmitted: toBareValue(data.bg) === true ? {} : { bg: false },
|
|
1877
2251
|
eventTargets,
|
|
1878
2252
|
requestTarget: {
|
|
1879
2253
|
sn: 0,
|
|
@@ -1952,6 +2326,14 @@ var CmcdReporter = class {
|
|
|
1952
2326
|
* a new session therefore always fires, even when the persisted value did
|
|
1953
2327
|
* not change across the `sid` boundary.
|
|
1954
2328
|
*
|
|
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`.
|
|
2336
|
+
*
|
|
1955
2337
|
* Multi-field updates fire the events in this order: `sta`, `pr`, `cid`,
|
|
1956
2338
|
* `bg`, `br`. The order of keys in the input object does not affect the
|
|
1957
2339
|
* firing order.
|
|
@@ -1971,8 +2353,7 @@ var CmcdReporter = class {
|
|
|
1971
2353
|
* writes them into every outgoing report, so this method is the only way
|
|
1972
2354
|
* to change them. A per-call value on {@link CmcdReporter.recordEvent},
|
|
1973
2355
|
* {@link CmcdReporter.createRequestReport}, or
|
|
1974
|
-
* {@link CmcdReporter.recordResponseReceived} has no effect.
|
|
1975
|
-
* attribution uses the provenance record alone.
|
|
2356
|
+
* {@link CmcdReporter.recordResponseReceived} has no effect.
|
|
1976
2357
|
* `msd` must be a finite number of milliseconds between `0` and
|
|
1977
2358
|
* `999_999_999_999_999` (the RFC 8941 integer maximum). The reporter
|
|
1978
2359
|
* rounds `msd` to the nearest integer and ignores invalid values.
|
|
@@ -2035,9 +2416,9 @@ var CmcdReporter = class {
|
|
|
2035
2416
|
* data store, as `update()` would.
|
|
2036
2417
|
* 2. Discards the event if the state field has no value after that write
|
|
2037
2418
|
* (never set, or cleared with `update({ field: undefined })`).
|
|
2038
|
-
* A state-change event without its required field would violate CTA-5004-B.
|
|
2039
2419
|
* 3. Suppresses the event if the field's current value equals the
|
|
2040
|
-
* last reported value (no state transition).
|
|
2420
|
+
* last reported value (no state transition). {@link CmcdReporter.update}
|
|
2421
|
+
* describes the `bg` value that a new session starts with.
|
|
2041
2422
|
*
|
|
2042
2423
|
* The reporter always records all other event types.
|
|
2043
2424
|
*
|
|
@@ -2123,6 +2504,7 @@ var CmcdReporter = class {
|
|
|
2123
2504
|
e: type,
|
|
2124
2505
|
ts: data.ts ?? Date.now()
|
|
2125
2506
|
};
|
|
2507
|
+
if (type === CMCD_EVENT_BACKGROUNDED_MODE && toBareValue(item.bg) === false) delete item.bg;
|
|
2126
2508
|
const { transform } = config;
|
|
2127
2509
|
if (!transform) {
|
|
2128
2510
|
this.queueTargetEvent(session, target, config, item, type);
|
|
@@ -2134,8 +2516,8 @@ var CmcdReporter = class {
|
|
|
2134
2516
|
const ts = item.ts;
|
|
2135
2517
|
const report = transform(item, request);
|
|
2136
2518
|
if (report == null) return;
|
|
2137
|
-
if (!
|
|
2138
|
-
if (requiredKey &&
|
|
2519
|
+
if (!isValid(report.ts)) report.ts = ts;
|
|
2520
|
+
if (requiredKey && isValid(requiredValue) && !isValid(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
|
|
2139
2521
|
this.queueTargetEvent(session, target, config, report, type);
|
|
2140
2522
|
}
|
|
2141
2523
|
/**
|
|
@@ -2189,27 +2571,24 @@ var CmcdReporter = class {
|
|
|
2189
2571
|
* TypeScript therefore rejects a call whose request the configured
|
|
2190
2572
|
* transforms could not read. See {@link CmcdReporterCustomData}.
|
|
2191
2573
|
*
|
|
2192
|
-
* The
|
|
2193
|
-
*
|
|
2194
|
-
* {@link
|
|
2195
|
-
*
|
|
2196
|
-
*
|
|
2197
|
-
*
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
* 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.
|
|
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.
|
|
2207
2586
|
*
|
|
2208
|
-
*
|
|
2209
|
-
* snapshot, which the reporter decodes for each response.
|
|
2210
|
-
* replaces the session's current `cid` in the report. A
|
|
2211
|
-
* completes after a mid-session content change therefore
|
|
2212
|
-
* request-time values.
|
|
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.
|
|
2213
2592
|
*
|
|
2214
2593
|
* @typeParam RD - The `customData` of this request. Defaults to the
|
|
2215
2594
|
* reporter's own `C`.
|
|
@@ -2218,17 +2597,17 @@ var CmcdReporter = class {
|
|
|
2218
2597
|
* @param data - Additional CMCD data to include with the event.
|
|
2219
2598
|
* Values provided here override the derived values.
|
|
2220
2599
|
* The reporter ignores session-owned keys (`sid`, `msd`)
|
|
2221
|
-
* supplied here.
|
|
2600
|
+
* supplied here.
|
|
2222
2601
|
*/
|
|
2223
2602
|
recordResponseReceived(response, data = {}) {
|
|
2224
2603
|
const { request } = response;
|
|
2225
2604
|
const url = data.url ?? request?.url;
|
|
2226
2605
|
if (!url) return;
|
|
2227
2606
|
const provenance = request.customData?.[CMCD_REQUEST_PROVENANCE];
|
|
2228
|
-
const session = this.resolveSession(provenance);
|
|
2607
|
+
const session = provenance === void 0 ? this.session : this.resolveSession(provenance);
|
|
2229
2608
|
if (!session) return;
|
|
2230
|
-
const cmcd = decodeSnapshot(provenance);
|
|
2231
|
-
const { cid } = provenance;
|
|
2609
|
+
const cmcd = provenance === void 0 ? { ...request.customData?.cmcd } : decodeSnapshot(provenance);
|
|
2610
|
+
const { cid } = provenance ?? {};
|
|
2232
2611
|
const urlObj = new URL(url);
|
|
2233
2612
|
urlObj.searchParams.delete(CMCD_PARAM);
|
|
2234
2613
|
const derived = {
|
|
@@ -2251,11 +2630,11 @@ var CmcdReporter = class {
|
|
|
2251
2630
|
}, request);
|
|
2252
2631
|
}
|
|
2253
2632
|
/**
|
|
2254
|
-
* Resolves the session a
|
|
2633
|
+
* Resolves the session that a provenance record names. The record's `sid`
|
|
2255
2634
|
* must name one of this reporter's retained sessions, or the reporter
|
|
2256
|
-
* discards the response.
|
|
2257
|
-
*
|
|
2258
|
-
*
|
|
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
|
|
2259
2638
|
* copied through JSON therefore resolves the same session, and a hand-built
|
|
2260
2639
|
* record that names a retained session is accepted.
|
|
2261
2640
|
*/
|
|
@@ -2544,6 +2923,63 @@ function fromCmcdUrl(url, options) {
|
|
|
2544
2923
|
return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")), options);
|
|
2545
2924
|
}
|
|
2546
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
|
+
|
|
2547
2983
|
//#endregion
|
|
2548
2984
|
//#region src/isCmcdV1Data.ts
|
|
2549
2985
|
/**
|
|
@@ -2562,6 +2998,25 @@ function isCmcdV1Data(data) {
|
|
|
2562
2998
|
return data.v !== CMCD_V2;
|
|
2563
2999
|
}
|
|
2564
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
|
+
|
|
2565
3020
|
//#endregion
|
|
2566
3021
|
//#region src/isCmcdV2Data.ts
|
|
2567
3022
|
/**
|
|
@@ -2619,7 +3074,7 @@ function mergeValidationResults(...results) {
|
|
|
2619
3074
|
*/
|
|
2620
3075
|
function resolveVersion(data, options) {
|
|
2621
3076
|
if (options?.version === 1 || options?.version === 2) return options.version;
|
|
2622
|
-
const payloadVersion = data["v"];
|
|
3077
|
+
const payloadVersion = toBareValue(data["v"]);
|
|
2623
3078
|
if (payloadVersion === 1 || payloadVersion === 2) return payloadVersion;
|
|
2624
3079
|
return CMCD_V1;
|
|
2625
3080
|
}
|
|
@@ -2660,6 +3115,51 @@ function validateCmcdKeys(data, options) {
|
|
|
2660
3115
|
};
|
|
2661
3116
|
}
|
|
2662
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
|
+
|
|
2663
3163
|
//#endregion
|
|
2664
3164
|
//#region src/validateCmcdStructure.ts
|
|
2665
3165
|
/**
|
|
@@ -2727,9 +3227,9 @@ function validateCmcdStructure(data, options) {
|
|
|
2727
3227
|
message: `Response key "${key}" must only be present when e="rr".`,
|
|
2728
3228
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2729
3229
|
});
|
|
2730
|
-
|
|
2731
|
-
key:
|
|
2732
|
-
message:
|
|
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.",
|
|
2733
3233
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2734
3234
|
});
|
|
2735
3235
|
if (eventType === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
|
|
@@ -2755,9 +3255,10 @@ function validateCmcdStructure(data, options) {
|
|
|
2755
3255
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2756
3256
|
});
|
|
2757
3257
|
}
|
|
2758
|
-
|
|
3258
|
+
const v = toBareValue(data["v"]);
|
|
3259
|
+
if ("v" in data && v !== 1 && v !== 2) issues.push({
|
|
2759
3260
|
key: "v",
|
|
2760
|
-
message: `Unsupported CMCD version "${String(
|
|
3261
|
+
message: `Unsupported CMCD version "${String(v)}". Expected 1 or 2.`,
|
|
2761
3262
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2762
3263
|
});
|
|
2763
3264
|
else if (version > 1 && !("v" in data)) issues.push({
|
|
@@ -2779,7 +3280,7 @@ function validateCmcdStructure(data, options) {
|
|
|
2779
3280
|
//#endregion
|
|
2780
3281
|
//#region src/CMCD_KEY_TYPES.ts
|
|
2781
3282
|
/**
|
|
2782
|
-
* CMCD key value type: inner list of
|
|
3283
|
+
* CMCD key value type: inner list of integers with token identifiers.
|
|
2783
3284
|
*
|
|
2784
3285
|
* @internal
|
|
2785
3286
|
*/
|
|
@@ -2886,9 +3387,9 @@ const CMCD_KEY_TYPES = {
|
|
|
2886
3387
|
*/
|
|
2887
3388
|
const CMCD_V1_KEY_TYPE_OVERRIDES = {
|
|
2888
3389
|
bl: CMCD_KEY_TYPE_INTEGER,
|
|
2889
|
-
br:
|
|
3390
|
+
br: CMCD_KEY_TYPE_INTEGER,
|
|
2890
3391
|
mtp: CMCD_KEY_TYPE_INTEGER,
|
|
2891
|
-
tb:
|
|
3392
|
+
tb: CMCD_KEY_TYPE_INTEGER,
|
|
2892
3393
|
nor: CMCD_KEY_TYPE_STRING
|
|
2893
3394
|
};
|
|
2894
3395
|
|
|
@@ -2979,22 +3480,31 @@ const CMCD_TOKEN_VALUES = {
|
|
|
2979
3480
|
|
|
2980
3481
|
//#endregion
|
|
2981
3482
|
//#region src/validateCmcdValues.ts
|
|
2982
|
-
const
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
const INTEGER_ROUNDING_KEYS = /* @__PURE__ */ new Set([
|
|
2990
|
-
"br",
|
|
2991
|
-
"d",
|
|
2992
|
-
"tb"
|
|
2993
|
-
]);
|
|
3483
|
+
const HUNDRED_ROUNDING_UNITS = {
|
|
3484
|
+
bl: "ms",
|
|
3485
|
+
dl: "ms",
|
|
3486
|
+
mtp: "kbps",
|
|
3487
|
+
rtp: "kbps",
|
|
3488
|
+
tbl: "ms"
|
|
3489
|
+
};
|
|
2994
3490
|
function isFiniteNumber(value) {
|
|
2995
3491
|
return typeof value === "number" && Number.isFinite(value);
|
|
2996
3492
|
}
|
|
2997
|
-
function
|
|
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) {
|
|
2998
3508
|
if (!Array.isArray(value)) {
|
|
2999
3509
|
issues.push({
|
|
3000
3510
|
key,
|
|
@@ -3004,18 +3514,13 @@ function validateListValue(key, value, issues) {
|
|
|
3004
3514
|
return;
|
|
3005
3515
|
}
|
|
3006
3516
|
for (let i = 0; i < value.length; i++) {
|
|
3007
|
-
const element = value[i];
|
|
3008
|
-
if (element
|
|
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({
|
|
3517
|
+
const element = toBareValue(value[i]);
|
|
3518
|
+
if (!isInteger(element)) issues.push({
|
|
3015
3519
|
key,
|
|
3016
|
-
message: `Key "${key}" array element [${i}] must be a finite
|
|
3520
|
+
message: `Key "${key}" array element [${i}] must be a finite integer.`,
|
|
3017
3521
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3018
3522
|
});
|
|
3523
|
+
else validateRounding(key, element, version, issues, i);
|
|
3019
3524
|
}
|
|
3020
3525
|
}
|
|
3021
3526
|
function validateStringArrayValue(key, value, issues) {
|
|
@@ -3027,20 +3532,11 @@ function validateStringArrayValue(key, value, issues) {
|
|
|
3027
3532
|
});
|
|
3028
3533
|
return;
|
|
3029
3534
|
}
|
|
3030
|
-
for (let i = 0; i < value.length; i++) {
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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
|
-
}
|
|
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
|
+
});
|
|
3044
3540
|
}
|
|
3045
3541
|
/**
|
|
3046
3542
|
* Validates that all values in a CMCD payload conform to the expected types and constraints.
|
|
@@ -3059,7 +3555,8 @@ function validateStringArrayValue(key, value, issues) {
|
|
|
3059
3555
|
function validateCmcdValues(data, options) {
|
|
3060
3556
|
const version = resolveVersion(data, options);
|
|
3061
3557
|
const issues = [];
|
|
3062
|
-
for (const [key,
|
|
3558
|
+
for (const [key, entry] of Object.entries(data)) {
|
|
3559
|
+
const value = toBareValue(entry);
|
|
3063
3560
|
if (isCmcdCustomKey(key)) {
|
|
3064
3561
|
if (typeof value !== "string") issues.push({
|
|
3065
3562
|
key,
|
|
@@ -3086,16 +3583,12 @@ function validateCmcdValues(data, options) {
|
|
|
3086
3583
|
if (version === CMCD_V1 && key in CMCD_V1_KEY_TYPE_OVERRIDES) expectedType = CMCD_V1_KEY_TYPE_OVERRIDES[key];
|
|
3087
3584
|
switch (expectedType) {
|
|
3088
3585
|
case CMCD_KEY_TYPE_INTEGER:
|
|
3089
|
-
if (!
|
|
3586
|
+
if (!isInteger(value)) issues.push({
|
|
3090
3587
|
key,
|
|
3091
3588
|
message: `Key "${key}" must be a finite integer.`,
|
|
3092
3589
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3093
3590
|
});
|
|
3094
|
-
else
|
|
3095
|
-
key,
|
|
3096
|
-
message: `Key "${key}" should be rounded to the nearest 100.`,
|
|
3097
|
-
severity: CMCD_VALIDATION_SEVERITY_WARNING
|
|
3098
|
-
});
|
|
3591
|
+
else validateRounding(key, value, version, issues);
|
|
3099
3592
|
break;
|
|
3100
3593
|
case CMCD_KEY_TYPE_NUMBER:
|
|
3101
3594
|
if (!isFiniteNumber(value)) issues.push({
|
|
@@ -3103,16 +3596,6 @@ function validateCmcdValues(data, options) {
|
|
|
3103
3596
|
message: `Key "${key}" must be a finite number.`,
|
|
3104
3597
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3105
3598
|
});
|
|
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
3599
|
break;
|
|
3117
3600
|
case CMCD_KEY_TYPE_BOOLEAN:
|
|
3118
3601
|
if (typeof value !== "boolean") issues.push({
|
|
@@ -3144,7 +3627,7 @@ function validateCmcdValues(data, options) {
|
|
|
3144
3627
|
break;
|
|
3145
3628
|
}
|
|
3146
3629
|
case CMCD_KEY_TYPE_NUMBER_LIST:
|
|
3147
|
-
validateListValue(key, value, issues);
|
|
3630
|
+
validateListValue(key, value, version, issues);
|
|
3148
3631
|
break;
|
|
3149
3632
|
case CMCD_KEY_TYPE_STRING_LIST:
|
|
3150
3633
|
validateStringArrayValue(key, value, issues);
|