@svta/cml-cmcd 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,11 +62,11 @@ const CMCD_V2 = 2;
62
62
  //#endregion
63
63
  //#region src/CmcdEventType.ts
64
64
  /**
65
- * CMCD event type for the 'br' key (bitrate change).
65
+ * CMCD event type for the 'bc' key (bitrate change).
66
66
  *
67
67
  * @public
68
68
  */
69
- const CMCD_EVENT_BITRATE_CHANGE = "br";
69
+ const CMCD_EVENT_BITRATE_CHANGE = "bc";
70
70
  /**
71
71
  * CMCD event type for the 'ps' key (play state change).
72
72
  *
@@ -166,9 +166,10 @@ const CMCD_EVENT_CUSTOM_EVENT = "ce";
166
166
  /**
167
167
  * CMCD event types for the 'e' key (event mode).
168
168
  *
169
- *
170
169
  * @enum
171
170
  *
171
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-A Event}
172
+ *
172
173
  * @public
173
174
  */
174
175
  const CmcdEventType = {
@@ -297,6 +298,7 @@ function isCmcdCustomKey(key) {
297
298
 
298
299
  //#endregion
299
300
  //#region src/isCmcdRequestKey.ts
301
+ const CMCD_REQUEST_KEY_SET = new Set(CMCD_REQUEST_KEYS);
300
302
  /**
301
303
  * Check if a key is a valid CMCD request key.
302
304
  *
@@ -310,7 +312,7 @@ function isCmcdCustomKey(key) {
310
312
  * {@includeCode ../test/isCmcdRequestKey.test.ts#example}
311
313
  */
312
314
  function isCmcdRequestKey(key) {
313
- return CMCD_REQUEST_KEYS.includes(key) || isCmcdCustomKey(key);
315
+ return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
314
316
  }
315
317
 
316
318
  //#endregion
@@ -333,6 +335,7 @@ const CMCD_RESPONSE_KEYS = [
333
335
 
334
336
  //#endregion
335
337
  //#region src/isCmcdResponseReceivedKey.ts
338
+ const CMCD_RESPONSE_KEY_SET = new Set(CMCD_RESPONSE_KEYS);
336
339
  /**
337
340
  * Check if a key is a valid CMCD response key.
338
341
  *
@@ -346,11 +349,12 @@ const CMCD_RESPONSE_KEYS = [
346
349
  * {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
347
350
  */
348
351
  function isCmcdResponseReceivedKey(key) {
349
- return CMCD_RESPONSE_KEYS.includes(key);
352
+ return CMCD_RESPONSE_KEY_SET.has(key);
350
353
  }
351
354
 
352
355
  //#endregion
353
356
  //#region src/isCmcdEventKey.ts
357
+ const CMCD_EVENT_KEY_SET = new Set(CMCD_EVENT_KEYS);
354
358
  /**
355
359
  * Check if a key is a valid CMCD event key.
356
360
  *
@@ -364,9 +368,35 @@ function isCmcdResponseReceivedKey(key) {
364
368
  * {@includeCode ../test/isCmcdEventKey.test.ts#example}
365
369
  */
366
370
  function isCmcdEventKey(key) {
367
- return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEYS.includes(key);
371
+ return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
368
372
  }
369
373
 
374
+ //#endregion
375
+ //#region src/CMCD_INNER_LIST_KEYS.ts
376
+ /**
377
+ * Keys that are inner lists in V2 but plain scalars in V1.
378
+ *
379
+ * Used by both encoding (down-conversion) and decoding (up-conversion).
380
+ *
381
+ * @internal
382
+ */
383
+ const CMCD_INNER_LIST_KEYS = new Set([
384
+ "ab",
385
+ "bl",
386
+ "br",
387
+ "bsa",
388
+ "bsd",
389
+ "bsda",
390
+ "lab",
391
+ "lb",
392
+ "mtp",
393
+ "pb",
394
+ "tab",
395
+ "tb",
396
+ "tbl",
397
+ "tpb"
398
+ ]);
399
+
370
400
  //#endregion
371
401
  //#region src/CMCD_V1_KEYS.ts
372
402
  /**
@@ -397,6 +427,7 @@ const CMCD_V1_KEYS = [
397
427
 
398
428
  //#endregion
399
429
  //#region src/isCmcdV1Key.ts
430
+ const CMCD_V1_KEY_SET$1 = new Set(CMCD_V1_KEYS);
400
431
  /**
401
432
  * Filter function for CMCD v1 keys.
402
433
  *
@@ -410,11 +441,18 @@ const CMCD_V1_KEYS = [
410
441
  * {@includeCode ../test/isCmcdV1Key.test.ts#example}
411
442
  */
412
443
  function isCmcdV1Key(key) {
413
- return CMCD_V1_KEYS.includes(key) || isCmcdCustomKey(key);
444
+ return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
414
445
  }
415
446
 
416
447
  //#endregion
417
448
  //#region src/isTokenField.ts
449
+ const TOKEN_FIELDS = new Set([
450
+ "ot",
451
+ "sf",
452
+ "st",
453
+ "e",
454
+ "sta"
455
+ ]);
418
456
  /**
419
457
  * Checks if the given key is a token field.
420
458
  *
@@ -425,13 +463,7 @@ function isCmcdV1Key(key) {
425
463
  * @internal
426
464
  */
427
465
  function isTokenField(key) {
428
- return [
429
- "ot",
430
- "sf",
431
- "st",
432
- "e",
433
- "sta"
434
- ].includes(key);
466
+ return TOKEN_FIELDS.has(key);
435
467
  }
436
468
 
437
469
  //#endregion
@@ -457,25 +489,6 @@ const filterMap = {
457
489
  [CMCD_REQUEST_MODE]: isCmcdRequestKey
458
490
  };
459
491
  /**
460
- * V1 keys that use inner lists in V2 but plain numbers in V1.
461
- */
462
- const INNER_LIST_V1_KEYS = new Set([
463
- "ab",
464
- "bl",
465
- "br",
466
- "bsa",
467
- "bsd",
468
- "bsda",
469
- "lab",
470
- "lb",
471
- "mtp",
472
- "pb",
473
- "tab",
474
- "tb",
475
- "tbl",
476
- "tpb"
477
- ]);
478
- /**
479
492
  * Unwrap an inner list or SfItem value to a plain scalar.
480
493
  */
481
494
  function unwrapValue(value, ot) {
@@ -507,7 +520,7 @@ function downConvertToV1(obj) {
507
520
  result["nor"] = first.value;
508
521
  if (first.params?.r) result["nrr"] = first.params.r;
509
522
  } else result["nor"] = first;
510
- } else if (INNER_LIST_V1_KEYS.has(key)) result[key] = unwrapValue(value, obj["ot"]);
523
+ } else if (CMCD_INNER_LIST_KEYS.has(key)) result[key] = unwrapValue(value, obj["ot"]);
511
524
  else result[key] = value;
512
525
  }
513
526
  return result;
@@ -527,37 +540,37 @@ function prepareCmcdData(obj, options = {}) {
527
540
  const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
528
541
  const data = version === 1 ? downConvertToV1(obj) : obj;
529
542
  const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode];
530
- let keys$1 = Object.keys(data).filter(keyFilter);
531
- if (data["e"] && data["e"] !== CMCD_EVENT_RESPONSE_RECEIVED) keys$1 = keys$1.filter((key) => !isCmcdResponseReceivedKey(key));
543
+ let keys = Object.keys(data).filter(keyFilter);
544
+ if (data["e"] && data["e"] !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
532
545
  const filter = options.filter;
533
- if (typeof filter === "function") keys$1 = keys$1.filter(filter);
546
+ if (typeof filter === "function") keys = keys.filter(filter);
534
547
  const isEventMode = reportingMode === CMCD_EVENT_MODE;
535
548
  if (isEventMode) {
536
549
  const eventType = data["e"];
537
- if (!keys$1.includes("e") && eventType != null) keys$1.push("e");
538
- if (!keys$1.includes("ts")) keys$1.push("ts");
539
- if (!keys$1.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys$1.push("cen");
550
+ if (!keys.includes("e") && eventType != null) keys.push("e");
551
+ if (!keys.includes("ts")) keys.push("ts");
552
+ if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
540
553
  }
541
- if (keys$1.length === 0) return results;
542
- if (version > 1 && !keys$1.includes("v")) keys$1.push("v");
543
- const formatters = Object.assign({}, CMCD_FORMATTER_MAP, options.formatters);
554
+ if (keys.length === 0) return results;
555
+ if (version > 1 && !keys.includes("v")) keys.push("v");
544
556
  const formatterOptions = {
545
557
  version,
546
558
  reportingMode,
547
559
  baseUrl: options.baseUrl
548
560
  };
549
- keys$1.sort().forEach((key) => {
561
+ keys.sort();
562
+ for (const key of keys) {
550
563
  let value = data[key];
551
- const formatter = formatters[key];
564
+ const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
552
565
  if (typeof formatter === "function") value = formatter(value, formatterOptions);
553
- if (key === "v") if (version === 1) return;
566
+ if (key === "v") if (version === 1) continue;
554
567
  else value = version;
555
- if (key == "pr" && value === 1) return;
568
+ if (key === "pr" && value === 1) continue;
556
569
  if (isEventMode && key === "ts" && !Number.isFinite(value)) value = Date.now();
557
- if (!isValid(value)) return;
570
+ if (!isValid(value)) continue;
558
571
  if (isTokenField(key) && typeof value === "string") value = new SfToken(value);
559
572
  results[key] = value;
560
- });
573
+ }
561
574
  return results;
562
575
  }
563
576
 
@@ -601,12 +614,27 @@ const CmcdHeaderField = {
601
614
  SESSION: CMCD_SESSION,
602
615
  STATUS: CMCD_STATUS
603
616
  };
617
+ /**
618
+ * All CMCD header fields as an array.
619
+ *
620
+ * @public
621
+ */
622
+ const CMCD_HEADER_FIELDS = [
623
+ CMCD_OBJECT,
624
+ CMCD_REQUEST,
625
+ CMCD_SESSION,
626
+ CMCD_STATUS
627
+ ];
604
628
 
605
629
  //#endregion
606
630
  //#region src/CMCD_HEADER_MAP.ts
607
631
  /**
608
632
  * The map of CMCD keys to their appropriate header shard.
609
633
  *
634
+ * Note: Event-only keys (e, ts, cen, h) and response-received keys
635
+ * (rc, ttfb, ttlb, url, etc.) are intentionally absent. They are
636
+ * transmitted via the event-mode POST body, not HTTP headers.
637
+ *
610
638
  * @public
611
639
  */
612
640
  const CMCD_HEADER_MAP = {
@@ -667,19 +695,21 @@ function createHeaderMap(headerMap) {
667
695
  *
668
696
  * @returns The CMCD header shards.
669
697
  *
698
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-A Header Field Definition}
699
+ *
670
700
  * @public
671
701
  */
672
702
  function groupCmcdHeaders(cmcd, customHeaderMap) {
673
703
  const result = {};
674
704
  if (!cmcd) return result;
675
- const keys$1 = Object.keys(cmcd);
705
+ const keys = Object.keys(cmcd);
676
706
  const custom = customHeaderMap ? createHeaderMap(customHeaderMap) : {};
677
- return keys$1.reduce((acc, key) => {
707
+ for (const key of keys) {
678
708
  const field = CMCD_HEADER_MAP[key] || custom[key] || CmcdHeaderField.REQUEST;
679
- const data = acc[field] ??= {};
709
+ const data = result[field] ??= {};
680
710
  data[key] = cmcd[key];
681
- return acc;
682
- }, result);
711
+ }
712
+ return result;
683
713
  }
684
714
 
685
715
  //#endregion
@@ -696,11 +726,11 @@ function groupCmcdHeaders(cmcd, customHeaderMap) {
696
726
  function toPreparedCmcdHeaders(data, customHeaderMap) {
697
727
  const result = {};
698
728
  const shards = groupCmcdHeaders(data, customHeaderMap);
699
- return Object.entries(shards).reduce((acc, [field, value]) => {
729
+ for (const [field, value] of Object.entries(shards)) {
700
730
  const shard = encodeSfDict(value, { whitespace: false });
701
- if (shard) acc[field] = shard;
702
- return acc;
703
- }, result);
731
+ if (shard) result[field] = shard;
732
+ }
733
+ return result;
704
734
  }
705
735
 
706
736
  //#endregion
@@ -713,6 +743,8 @@ function toPreparedCmcdHeaders(data, customHeaderMap) {
713
743
  *
714
744
  * @returns The CMCD header shards.
715
745
  *
746
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-A Header Field Definition}
747
+ *
716
748
  * @public
717
749
  *
718
750
  * @example
@@ -738,6 +770,8 @@ function toCmcdHeaders(cmcd, options = {}) {
738
770
  *
739
771
  * @example
740
772
  * {@includeCode ../test/appendCmcdHeaders.test.ts#example}
773
+ *
774
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-A Header Field Definition}
741
775
  */
742
776
  function appendCmcdHeaders(headers, cmcd, options) {
743
777
  return Object.assign(headers, toCmcdHeaders(cmcd, options));
@@ -776,6 +810,8 @@ function encodePreparedCmcd(data) {
776
810
  *
777
811
  * @returns The encoded CMCD string.
778
812
  *
813
+ * @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-A Payload Definition}
814
+ *
779
815
  * @public
780
816
  *
781
817
  * @example
@@ -796,6 +832,8 @@ function encodeCmcd(cmcd, options = {}) {
796
832
  *
797
833
  * @returns The URL encoded CMCD data.
798
834
  *
835
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-A Query Argument Definition}
836
+ *
799
837
  * @public
800
838
  */
801
839
  function toCmcdUrl(cmcd, options = {}) {
@@ -814,6 +852,8 @@ function toCmcdUrl(cmcd, options = {}) {
814
852
  *
815
853
  * @returns The CMCD query arg.
816
854
  *
855
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-A Query Argument Definition}
856
+ *
817
857
  * @public
818
858
  *
819
859
  * @example
@@ -840,6 +880,8 @@ const REGEX = /CMCD=[^&#]+/;
840
880
  *
841
881
  * @example
842
882
  * {@includeCode ../test/appendCmcdQuery.test.ts#example}
883
+ *
884
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-A Query Argument Definition}
843
885
  */
844
886
  function appendCmcdQuery(url, cmcd, options) {
845
887
  const query = toCmcdQuery(cmcd, options);
@@ -859,18 +901,26 @@ const CMCD_DEFAULT_TIME_INTERVAL = 30;
859
901
 
860
902
  //#endregion
861
903
  //#region src/CMCD_KEYS.ts
862
- const keySet = new Set([
904
+ /**
905
+ * A list of all CMCD keys.
906
+ *
907
+ * @public
908
+ */
909
+ const CMCD_KEYS = [
863
910
  ...CMCD_V1_KEYS,
864
911
  ...CMCD_REQUEST_KEYS,
865
912
  ...CMCD_RESPONSE_KEYS,
866
913
  ...CMCD_EVENT_KEYS
867
- ]);
914
+ ].filter((key, index, arr) => arr.indexOf(key) === index);
915
+
916
+ //#endregion
917
+ //#region src/CMCD_MIME_TYPE.ts
868
918
  /**
869
- * A list of all CMCD keys.
919
+ * CMCD MIME type for event report payloads.
870
920
  *
871
921
  * @public
872
922
  */
873
- const CMCD_KEYS = Array.from(keySet);
923
+ const CMCD_MIME_TYPE = "application/cmcd";
874
924
 
875
925
  //#endregion
876
926
  //#region src/CMCD_V1.ts
@@ -889,6 +939,8 @@ const CMCD_V1 = 1;
889
939
  * @public
890
940
  *
891
941
  * @enum
942
+ *
943
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#object-type | CTA-5004-A Object Type}
892
944
  */
893
945
  const CmcdObjectType = {
894
946
  MANIFEST: "m",
@@ -911,6 +963,8 @@ const CmcdObjectType = {
911
963
  * @enum
912
964
  *
913
965
  * @public
966
+ *
967
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#state | CTA-5004-A State}
914
968
  */
915
969
  const CmcdPlayerState = {
916
970
  STARTING: "s",
@@ -963,11 +1017,11 @@ const CmcdTransmissionMode = {
963
1017
  //#endregion
964
1018
  //#region src/CmcdReporter.ts
965
1019
  function createEncodingOptions(reportingMode, config, baseUrl) {
966
- const { enabledKeys = [] } = config;
1020
+ const enabledKeySet = new Set(config.enabledKeys ?? []);
967
1021
  return {
968
1022
  version: config.version || CMCD_V2,
969
1023
  reportingMode,
970
- filter: (key) => enabledKeys.includes(key),
1024
+ filter: (key) => enabledKeySet.has(key),
971
1025
  baseUrl
972
1026
  };
973
1027
  }
@@ -998,6 +1052,8 @@ function createCmcdReporterConfig(config) {
998
1052
  /**
999
1053
  * The CMCD reporter.
1000
1054
  *
1055
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#reporting-modes-when-we-send-data | CTA-5004-A Reporting Modes}
1056
+ *
1001
1057
  * @public
1002
1058
  */
1003
1059
  var CmcdReporter = class {
@@ -1034,6 +1090,10 @@ var CmcdReporter = class {
1034
1090
  }
1035
1091
  /**
1036
1092
  * Starts the CMCD reporter. Called by the player when the reporter is enabled.
1093
+ *
1094
+ * Note: This fires an initial time-interval event immediately (synchronously)
1095
+ * before the first interval elapses. Ensure CMCD data (sid, cid, etc.) is
1096
+ * populated before calling start().
1037
1097
  */
1038
1098
  start() {
1039
1099
  this.eventTargets.forEach((target, config) => {
@@ -1071,10 +1131,7 @@ var CmcdReporter = class {
1071
1131
  * @param data - The data to update.
1072
1132
  */
1073
1133
  update(data) {
1074
- if (data.sid && data.sid !== this.data.sid) {
1075
- this.resetSession();
1076
- return;
1077
- }
1134
+ if (data.sid && data.sid !== this.data.sid) this.resetSession();
1078
1135
  if (data.msd && !isNaN(data.msd)) this.msd = data.msd;
1079
1136
  this.data = {
1080
1137
  ...this.data,
@@ -1189,7 +1246,7 @@ var CmcdReporter = class {
1189
1246
  * Creates a new request with the CMCD request report data applied. Called by the player
1190
1247
  * before sending the request.
1191
1248
  *
1192
- * @param req - The request to apply the CMCD request report to.
1249
+ * @param request - The request to apply the CMCD request report to.
1193
1250
  * @param data - The data to apply to the request. This data only
1194
1251
  * applies to this request report. Persistent data
1195
1252
  * should be updated using `update()`.
@@ -1245,7 +1302,9 @@ var CmcdReporter = class {
1245
1302
  if (queue.length < config.batchSize && !flush) return;
1246
1303
  const deleteCount = flush ? queue.length : config.batchSize;
1247
1304
  const events = queue.splice(0, deleteCount);
1248
- this.sendEventReport(config, events);
1305
+ this.sendEventReport(config, events).catch(() => {
1306
+ target.queue.unshift(...events);
1307
+ });
1249
1308
  reprocess ||= queue.length > 0;
1250
1309
  });
1251
1310
  if (reprocess) this.processEventTargets();
@@ -1261,18 +1320,11 @@ var CmcdReporter = class {
1261
1320
  const { status } = await this.requester({
1262
1321
  url: target.url,
1263
1322
  method: "POST",
1264
- headers: { "Content-Type": "text/cmcd" },
1265
- body: data.reduce((acc, item) => acc += `${encodeCmcd(item, options)}\n`, "")
1323
+ headers: { "Content-Type": CMCD_MIME_TYPE },
1324
+ body: data.map((item) => encodeCmcd(item, options)).join("\n") + "\n"
1266
1325
  });
1267
- switch (status) {
1268
- case 410:
1269
- this.eventTargets.delete(target);
1270
- break;
1271
- case 429: break;
1272
- default:
1273
- if (status > 499 && status < 600) {}
1274
- break;
1275
- }
1326
+ if (status === 410) this.eventTargets.delete(target);
1327
+ else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
1276
1328
  }
1277
1329
  /**
1278
1330
  * Resets the session related data. Called when the session ID changes.
@@ -1288,9 +1340,10 @@ var CmcdReporter = class {
1288
1340
  /**
1289
1341
  * Common Media Client Data Streaming Format
1290
1342
  *
1291
- *
1292
1343
  * @enum
1293
1344
  *
1345
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#streaming-format | CTA-5004-A Streaming Format}
1346
+ *
1294
1347
  * @public
1295
1348
  */
1296
1349
  const CmcdStreamingFormat = {
@@ -1307,6 +1360,8 @@ const CmcdStreamingFormat = {
1307
1360
  *
1308
1361
  * @enum
1309
1362
  *
1363
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#stream-type | CTA-5004-A Stream Type}
1364
+ *
1310
1365
  * @public
1311
1366
  */
1312
1367
  const CmcdStreamType = {
@@ -1315,116 +1370,993 @@ const CmcdStreamType = {
1315
1370
  LOW_LATENCY: "ll"
1316
1371
  };
1317
1372
 
1373
+ //#endregion
1374
+ //#region src/CmcdValidationSeverity.ts
1375
+ /**
1376
+ * CMCD validation severity level: error.
1377
+ *
1378
+ * @public
1379
+ */
1380
+ const CMCD_VALIDATION_SEVERITY_ERROR = "error";
1381
+ /**
1382
+ * CMCD validation severity level: warning.
1383
+ *
1384
+ * @public
1385
+ */
1386
+ const CMCD_VALIDATION_SEVERITY_WARNING = "warning";
1387
+ /**
1388
+ * CMCD validation severity level.
1389
+ *
1390
+ * @public
1391
+ */
1392
+ const CmcdValidationSeverity = {
1393
+ ERROR: CMCD_VALIDATION_SEVERITY_ERROR,
1394
+ WARNING: CMCD_VALIDATION_SEVERITY_WARNING
1395
+ };
1396
+
1397
+ //#endregion
1398
+ //#region src/upConvertToV2.ts
1399
+ /**
1400
+ * Up-convert V1 CMCD data to V2 format.
1401
+ *
1402
+ * - Wraps plain scalar values in arrays for inner-list keys.
1403
+ * - Wraps `nor` string in an array.
1404
+ *
1405
+ * If the data is already V2 (has `v: 2`), it is returned unchanged.
1406
+ *
1407
+ * @internal
1408
+ */
1409
+ function upConvertToV2(obj) {
1410
+ if (obj["v"] === CMCD_V2) return obj;
1411
+ const result = {};
1412
+ for (const [key, value] of Object.entries(obj)) {
1413
+ if (value == null) {
1414
+ result[key] = value;
1415
+ continue;
1416
+ }
1417
+ if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
1418
+ else if (key === "nor" && typeof value === "string") result[key] = [value];
1419
+ else result[key] = value;
1420
+ }
1421
+ return result;
1422
+ }
1423
+
1318
1424
  //#endregion
1319
1425
  //#region src/decodeCmcd.ts
1320
1426
  function reduceValue(value) {
1321
1427
  if (Array.isArray(value)) return value.map(reduceValue);
1322
1428
  if (typeof value === "symbol") return symbolToStr(value);
1323
1429
  if (value instanceof SfItem && !value.params) return reduceValue(value.value);
1324
- if (typeof value === "string") return decodeURIComponent(value);
1430
+ if (typeof value === "string") return value;
1325
1431
  return value;
1326
1432
  }
1433
+ function decodeCmcd(cmcd, options) {
1434
+ if (!cmcd) return {};
1435
+ const sfDict = decodeSfDict(cmcd);
1436
+ const result = {};
1437
+ for (const [key, item] of Object.entries(sfDict)) result[key] = reduceValue(item.value);
1438
+ if (options?.convertToLatest) return upConvertToV2(result);
1439
+ return result;
1440
+ }
1441
+
1442
+ //#endregion
1443
+ //#region src/ensureHeaders.ts
1444
+ /**
1445
+ * Converts a record of header fields to a `Headers` instance if necessary.
1446
+ *
1447
+ * @param headers - A `Headers` instance or a plain record of header fields.
1448
+ * @returns A `Headers` instance.
1449
+ *
1450
+ * @internal
1451
+ */
1452
+ function ensureHeaders(headers) {
1453
+ return headers instanceof Headers ? headers : new Headers(headers);
1454
+ }
1455
+
1456
+ //#endregion
1457
+ //#region src/fromCmcdHeaders.ts
1458
+ function fromCmcdHeaders(headers, options) {
1459
+ const h = ensureHeaders(headers);
1460
+ const result = {};
1461
+ for (const field of CMCD_HEADER_FIELDS) {
1462
+ const value = h.get(field);
1463
+ if (value) Object.assign(result, decodeCmcd(value));
1464
+ }
1465
+ if (options?.convertToLatest) return upConvertToV2(result);
1466
+ return result;
1467
+ }
1468
+
1469
+ //#endregion
1470
+ //#region src/fromCmcdQuery.ts
1471
+ function fromCmcdQuery(query, options) {
1472
+ if (typeof query === "string") query = new URLSearchParams(query);
1473
+ return decodeCmcd(query.get(CMCD_PARAM) ?? "", options);
1474
+ }
1475
+
1476
+ //#endregion
1477
+ //#region src/fromCmcdUrl.ts
1478
+ function fromCmcdUrl(url, options) {
1479
+ return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")), options);
1480
+ }
1481
+
1482
+ //#endregion
1483
+ //#region src/isCmcdV1Data.ts
1327
1484
  /**
1328
- * Decode a CMCD string to an object.
1485
+ * Check if a CMCD data object is version 1.
1329
1486
  *
1330
- * @param cmcd - The CMCD string to decode.
1487
+ * @param data - The CMCD data object to check.
1331
1488
  *
1332
- * @returns The decoded CMCD object.
1489
+ * @returns `true` if the data is version 1, `false` otherwise.
1333
1490
  *
1334
1491
  * @public
1335
1492
  *
1336
1493
  * @example
1337
- * {@includeCode ../test/decodeCmcd.test.ts#example}
1494
+ * {@includeCode ../test/isCmcdV1Data.test.ts#example}
1338
1495
  */
1339
- function decodeCmcd(cmcd) {
1340
- if (!cmcd) return {};
1341
- const sfDict = decodeSfDict(cmcd);
1342
- return Object.entries(sfDict).reduce((acc, [key, item]) => {
1343
- acc[key] = reduceValue(item.value);
1344
- return acc;
1345
- }, {});
1496
+ function isCmcdV1Data(data) {
1497
+ return data.v !== CMCD_V2;
1346
1498
  }
1347
1499
 
1348
1500
  //#endregion
1349
- //#region src/fromCmcdHeaders.ts
1350
- const keys = [
1351
- CMCD_OBJECT,
1352
- CMCD_REQUEST,
1353
- CMCD_SESSION,
1354
- CMCD_STATUS
1355
- ];
1501
+ //#region src/isCmcdV2Data.ts
1356
1502
  /**
1357
- * Decode CMCD data from request headers.
1503
+ * Check if a CMCD data object is version 2.
1358
1504
  *
1359
- * @param headers - The request headers to decode.
1505
+ * @param data - The CMCD data object to check.
1360
1506
  *
1361
- * @returns The decoded CMCD data.
1507
+ * @returns `true` if the data is version 2, `false` otherwise.
1362
1508
  *
1363
1509
  * @public
1364
1510
  *
1365
1511
  * @example
1366
- * {@includeCode ../test/fromCmcdHeaders.test.ts#example}
1512
+ * {@includeCode ../test/isCmcdV2Data.test.ts#example}
1367
1513
  */
1368
- function fromCmcdHeaders(headers) {
1369
- if (!(headers instanceof Headers)) headers = new Headers(headers);
1370
- return keys.reduce((acc, key) => {
1371
- const value = headers.get(key);
1372
- return Object.assign(acc, decodeCmcd(value));
1373
- }, {});
1514
+ function isCmcdV2Data(data) {
1515
+ return data.v === CMCD_V2;
1374
1516
  }
1375
1517
 
1376
1518
  //#endregion
1377
- //#region src/fromCmcdQuery.ts
1519
+ //#region src/toCmcdValue.ts
1378
1520
  /**
1379
- * Decode CMCD data from a query string.
1521
+ * Convert a value to a CMCD value.
1380
1522
  *
1381
- * @param query - The query string to decode.
1523
+ * @param value - The value to convert to a CMCD value.
1524
+ * @param params - The parameters to convert to a CMCD value.
1525
+ * @returns The CMCD value.
1382
1526
  *
1383
- * @returns The decoded CMCD data.
1527
+ * @public
1528
+ */
1529
+ function toCmcdValue(value, params) {
1530
+ return new SfItem(value, params);
1531
+ }
1532
+
1533
+ //#endregion
1534
+ //#region src/mergeValidationResults.ts
1535
+ /**
1536
+ * Merges multiple validation results into a single result.
1537
+ *
1538
+ * @internal
1539
+ */
1540
+ function mergeValidationResults(...results) {
1541
+ const issues = results.flatMap((r) => r.issues);
1542
+ return {
1543
+ valid: issues.every((i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR),
1544
+ issues
1545
+ };
1546
+ }
1547
+
1548
+ //#endregion
1549
+ //#region src/resolveVersion.ts
1550
+ /**
1551
+ * Resolves the CMCD version from explicit options, the payload's `v` key, or the default (v1).
1552
+ *
1553
+ * @internal
1554
+ */
1555
+ function resolveVersion(data, options) {
1556
+ if (options?.version === 1 || options?.version === 2) return options.version;
1557
+ const payloadVersion = data["v"];
1558
+ if (payloadVersion === 1 || payloadVersion === 2) return payloadVersion;
1559
+ return CMCD_V1;
1560
+ }
1561
+
1562
+ //#endregion
1563
+ //#region src/validateCmcdKeys.ts
1564
+ const CMCD_V1_KEY_SET = new Set(CMCD_V1_KEYS);
1565
+ const CMCD_KEY_SET = new Set(CMCD_KEYS);
1566
+ /**
1567
+ * Validates that all keys in a CMCD payload are recognized spec keys or valid custom keys.
1568
+ *
1569
+ * @example
1570
+ * {@includeCode ../test/validateCmcdKeys.test.ts#example}
1571
+ *
1572
+ * @param data - The CMCD payload to validate.
1573
+ * @param options - Validation options.
1574
+ * @returns The validation result.
1575
+ *
1576
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#reserved-keys | CTA-5004-A Reserved Keys}
1384
1577
  *
1385
1578
  * @public
1579
+ */
1580
+ function validateCmcdKeys(data, options) {
1581
+ const version = resolveVersion(data, options);
1582
+ const validKeySet = version === CMCD_V1 ? CMCD_V1_KEY_SET : CMCD_KEY_SET;
1583
+ const issues = [];
1584
+ for (const key of Object.keys(data)) {
1585
+ if (isCmcdCustomKey(key)) continue;
1586
+ if (!validKeySet.has(key)) issues.push({
1587
+ key,
1588
+ message: `Unknown CMCD key "${key}" for version ${version}.`,
1589
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1590
+ });
1591
+ }
1592
+ return {
1593
+ valid: issues.length === 0,
1594
+ issues
1595
+ };
1596
+ }
1597
+
1598
+ //#endregion
1599
+ //#region src/validateCmcdStructure.ts
1600
+ /**
1601
+ * Validates the structural rules of a CMCD payload.
1386
1602
  *
1387
1603
  * @example
1388
- * {@includeCode ../test/fromCmcdQuery.test.ts#example}
1604
+ * {@includeCode ../test/validateCmcdStructure.test.ts#example}
1605
+ *
1606
+ * @param data - The CMCD payload to validate.
1607
+ * @param options - Validation options.
1608
+ * @returns The validation result.
1609
+ *
1610
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#data-payload-definition-what-data-to-send | CTA-5004-A Data Payload Definition}
1611
+ *
1612
+ * @public
1389
1613
  */
1390
- function fromCmcdQuery(query) {
1391
- if (typeof query === "string") query = new URLSearchParams(query);
1392
- return decodeCmcd(query.get(CMCD_PARAM));
1614
+ function validateCmcdStructure(data, options) {
1615
+ const version = resolveVersion(data, options);
1616
+ const issues = [];
1617
+ if (options?.reportingMode === CMCD_REQUEST_MODE) {
1618
+ for (const key of CMCD_EVENT_KEYS) if (key in data) issues.push({
1619
+ key,
1620
+ message: `Event key "${key}" must not be present in request mode.`,
1621
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1622
+ });
1623
+ for (const key of CMCD_RESPONSE_KEYS) if (key in data) issues.push({
1624
+ key,
1625
+ message: `Response key "${key}" must not be present in request mode.`,
1626
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1627
+ });
1628
+ }
1629
+ if (options?.reportingMode === CMCD_EVENT_MODE) {
1630
+ if (!("e" in data)) issues.push({
1631
+ key: "e",
1632
+ message: "Event mode requires the \"e\" key to be present.",
1633
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1634
+ });
1635
+ if (!("ts" in data)) issues.push({
1636
+ key: "ts",
1637
+ message: "Event mode requires the \"ts\" key to be present.",
1638
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1639
+ });
1640
+ }
1641
+ if ("e" in data) {
1642
+ if (data["e"] === CMCD_EVENT_CUSTOM_EVENT) {
1643
+ if (!("cen" in data)) issues.push({
1644
+ key: "cen",
1645
+ message: "Custom event (e=\"ce\") requires the \"cen\" key to be present.",
1646
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1647
+ });
1648
+ } else if ("cen" in data) issues.push({
1649
+ key: "cen",
1650
+ message: "The \"cen\" key must only be present when e=\"ce\".",
1651
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1652
+ });
1653
+ if (data["e"] === CMCD_EVENT_RESPONSE_RECEIVED) {
1654
+ if (!("url" in data)) issues.push({
1655
+ key: "url",
1656
+ message: "Response received event (e=\"rr\") requires the \"url\" key to be present.",
1657
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1658
+ });
1659
+ } else for (const key of CMCD_RESPONSE_KEYS) if (key in data) issues.push({
1660
+ key,
1661
+ message: `Response key "${key}" must only be present when e="rr".`,
1662
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1663
+ });
1664
+ if (data["e"] === CMCD_EVENT_PLAY_STATE && !("sta" in data)) issues.push({
1665
+ key: "sta",
1666
+ message: "Play state event (e=\"ps\") requires the \"sta\" key to be present.",
1667
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1668
+ });
1669
+ if (data["e"] === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
1670
+ key: "ec",
1671
+ message: "Error event (e=\"e\") requires the \"ec\" key to be present.",
1672
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1673
+ });
1674
+ }
1675
+ if ("v" in data && data["v"] !== 1 && data["v"] !== 2) issues.push({
1676
+ key: "v",
1677
+ message: `Unsupported CMCD version "${String(data["v"])}". Expected 1 or 2.`,
1678
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1679
+ });
1680
+ else if (version > 1 && !("v" in data)) issues.push({
1681
+ key: "v",
1682
+ message: "Version 2 payloads require the \"v\" key to be present.",
1683
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1684
+ });
1685
+ else if (version === CMCD_V1 && "v" in data) issues.push({
1686
+ key: "v",
1687
+ message: "Version 1 payloads should omit the \"v\" key (v1 is the default).",
1688
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
1689
+ });
1690
+ return {
1691
+ valid: issues.every((i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR),
1692
+ issues
1693
+ };
1393
1694
  }
1394
1695
 
1395
1696
  //#endregion
1396
- //#region src/fromCmcdUrl.ts
1697
+ //#region src/CMCD_KEY_TYPES.ts
1698
+ /**
1699
+ * CMCD key value type: inner list of numbers with token identifiers.
1700
+ *
1701
+ * @internal
1702
+ */
1703
+ const CMCD_KEY_TYPE_NUMBER_LIST = "number[]";
1704
+ /**
1705
+ * CMCD key value type: inner list of strings.
1706
+ *
1707
+ * @internal
1708
+ */
1709
+ const CMCD_KEY_TYPE_STRING_LIST = "string[]";
1710
+ /**
1711
+ * CMCD key value type: integer.
1712
+ *
1713
+ * @internal
1714
+ */
1715
+ const CMCD_KEY_TYPE_INTEGER = "integer";
1397
1716
  /**
1398
- * Decode CMCD data from a url encoded string.
1717
+ * CMCD key value type: number (decimal).
1399
1718
  *
1400
- * @param url - The url encoded string to decode.
1719
+ * @internal
1720
+ */
1721
+ const CMCD_KEY_TYPE_NUMBER = "number";
1722
+ /**
1723
+ * CMCD key value type: boolean.
1401
1724
  *
1402
- * @returns The decoded CMCD data.
1725
+ * @internal
1726
+ */
1727
+ const CMCD_KEY_TYPE_BOOLEAN = "boolean";
1728
+ /**
1729
+ * CMCD key value type: string.
1730
+ *
1731
+ * @internal
1732
+ */
1733
+ const CMCD_KEY_TYPE_STRING = "string";
1734
+ /**
1735
+ * CMCD key value type: token.
1736
+ *
1737
+ * @internal
1738
+ */
1739
+ const CMCD_KEY_TYPE_TOKEN = "token";
1740
+ /**
1741
+ * Maps each CMCD spec key to its expected value type for v2.
1742
+ * Keys that differ between v1 and v2 are handled by CMCD_V1_KEY_TYPE_OVERRIDES.
1743
+ *
1744
+ * @internal
1745
+ */
1746
+ const CMCD_KEY_TYPES = {
1747
+ ab: CMCD_KEY_TYPE_NUMBER_LIST,
1748
+ bl: CMCD_KEY_TYPE_NUMBER_LIST,
1749
+ br: CMCD_KEY_TYPE_NUMBER_LIST,
1750
+ bsa: CMCD_KEY_TYPE_NUMBER_LIST,
1751
+ bsd: CMCD_KEY_TYPE_NUMBER_LIST,
1752
+ bsda: CMCD_KEY_TYPE_NUMBER_LIST,
1753
+ lab: CMCD_KEY_TYPE_NUMBER_LIST,
1754
+ lb: CMCD_KEY_TYPE_NUMBER_LIST,
1755
+ mtp: CMCD_KEY_TYPE_NUMBER_LIST,
1756
+ pb: CMCD_KEY_TYPE_NUMBER_LIST,
1757
+ tab: CMCD_KEY_TYPE_NUMBER_LIST,
1758
+ tb: CMCD_KEY_TYPE_NUMBER_LIST,
1759
+ tbl: CMCD_KEY_TYPE_NUMBER_LIST,
1760
+ tpb: CMCD_KEY_TYPE_NUMBER_LIST,
1761
+ ec: CMCD_KEY_TYPE_STRING_LIST,
1762
+ nor: CMCD_KEY_TYPE_STRING_LIST,
1763
+ d: CMCD_KEY_TYPE_INTEGER,
1764
+ dfa: CMCD_KEY_TYPE_INTEGER,
1765
+ dl: CMCD_KEY_TYPE_INTEGER,
1766
+ ltc: CMCD_KEY_TYPE_INTEGER,
1767
+ msd: CMCD_KEY_TYPE_INTEGER,
1768
+ pt: CMCD_KEY_TYPE_INTEGER,
1769
+ rc: CMCD_KEY_TYPE_INTEGER,
1770
+ rtp: CMCD_KEY_TYPE_INTEGER,
1771
+ sn: CMCD_KEY_TYPE_INTEGER,
1772
+ ts: CMCD_KEY_TYPE_INTEGER,
1773
+ ttfb: CMCD_KEY_TYPE_INTEGER,
1774
+ ttfbb: CMCD_KEY_TYPE_INTEGER,
1775
+ ttlb: CMCD_KEY_TYPE_INTEGER,
1776
+ v: CMCD_KEY_TYPE_INTEGER,
1777
+ pr: CMCD_KEY_TYPE_NUMBER,
1778
+ bg: CMCD_KEY_TYPE_BOOLEAN,
1779
+ bs: CMCD_KEY_TYPE_BOOLEAN,
1780
+ nr: CMCD_KEY_TYPE_BOOLEAN,
1781
+ su: CMCD_KEY_TYPE_BOOLEAN,
1782
+ cdn: CMCD_KEY_TYPE_STRING,
1783
+ cen: CMCD_KEY_TYPE_STRING,
1784
+ cid: CMCD_KEY_TYPE_STRING,
1785
+ cmsdd: CMCD_KEY_TYPE_STRING,
1786
+ cmsds: CMCD_KEY_TYPE_STRING,
1787
+ cs: CMCD_KEY_TYPE_STRING,
1788
+ h: CMCD_KEY_TYPE_STRING,
1789
+ nrr: CMCD_KEY_TYPE_STRING,
1790
+ sid: CMCD_KEY_TYPE_STRING,
1791
+ smrt: CMCD_KEY_TYPE_STRING,
1792
+ url: CMCD_KEY_TYPE_STRING,
1793
+ e: CMCD_KEY_TYPE_TOKEN,
1794
+ ot: CMCD_KEY_TYPE_TOKEN,
1795
+ sf: CMCD_KEY_TYPE_TOKEN,
1796
+ st: CMCD_KEY_TYPE_TOKEN,
1797
+ sta: CMCD_KEY_TYPE_TOKEN
1798
+ };
1799
+ /**
1800
+ * Maps keys to their v1-specific types when they differ from v2.
1801
+ *
1802
+ * @internal
1803
+ */
1804
+ const CMCD_V1_KEY_TYPE_OVERRIDES = {
1805
+ bl: CMCD_KEY_TYPE_INTEGER,
1806
+ br: CMCD_KEY_TYPE_NUMBER,
1807
+ mtp: CMCD_KEY_TYPE_INTEGER,
1808
+ tb: CMCD_KEY_TYPE_NUMBER,
1809
+ nor: CMCD_KEY_TYPE_STRING
1810
+ };
1811
+
1812
+ //#endregion
1813
+ //#region src/CMCD_STRING_LENGTH_LIMITS.ts
1814
+ /**
1815
+ * Maps CMCD keys to their maximum string length.
1816
+ *
1817
+ * @internal
1818
+ */
1819
+ const CMCD_STRING_LENGTH_LIMITS = {
1820
+ sid: 64,
1821
+ cid: 128,
1822
+ cdn: 128,
1823
+ h: 128,
1824
+ cen: 64
1825
+ };
1826
+ /**
1827
+ * Maximum length for custom key values.
1828
+ *
1829
+ * @internal
1830
+ */
1831
+ const CMCD_CUSTOM_KEY_VALUE_MAX_LENGTH = 64;
1832
+
1833
+ //#endregion
1834
+ //#region src/CMCD_TOKEN_VALUES.ts
1835
+ /**
1836
+ * Maps token keys to their valid values.
1837
+ *
1838
+ * @internal
1839
+ */
1840
+ const CMCD_TOKEN_VALUES = {
1841
+ e: [
1842
+ "bc",
1843
+ "ps",
1844
+ "e",
1845
+ "t",
1846
+ "c",
1847
+ "b",
1848
+ "m",
1849
+ "um",
1850
+ "pe",
1851
+ "pc",
1852
+ "rr",
1853
+ "as",
1854
+ "ae",
1855
+ "abs",
1856
+ "abe",
1857
+ "sk",
1858
+ "ce"
1859
+ ],
1860
+ ot: [
1861
+ "m",
1862
+ "a",
1863
+ "v",
1864
+ "av",
1865
+ "i",
1866
+ "c",
1867
+ "tt",
1868
+ "k",
1869
+ "o"
1870
+ ],
1871
+ sf: [
1872
+ "d",
1873
+ "h",
1874
+ "s",
1875
+ "o"
1876
+ ],
1877
+ st: [
1878
+ "v",
1879
+ "l",
1880
+ "ll"
1881
+ ],
1882
+ sta: [
1883
+ "s",
1884
+ "p",
1885
+ "k",
1886
+ "r",
1887
+ "a",
1888
+ "w",
1889
+ "e",
1890
+ "f",
1891
+ "q",
1892
+ "d"
1893
+ ]
1894
+ };
1895
+
1896
+ //#endregion
1897
+ //#region src/validateCmcdValues.ts
1898
+ const HUNDRED_ROUNDING_KEYS = new Set([
1899
+ "bl",
1900
+ "dl",
1901
+ "mtp",
1902
+ "rtp",
1903
+ "tbl"
1904
+ ]);
1905
+ const INTEGER_ROUNDING_KEYS = new Set([
1906
+ "br",
1907
+ "d",
1908
+ "tb"
1909
+ ]);
1910
+ function isFiniteNumber(value) {
1911
+ return typeof value === "number" && Number.isFinite(value);
1912
+ }
1913
+ function validateListValue(key, value, issues) {
1914
+ if (!Array.isArray(value)) {
1915
+ issues.push({
1916
+ key,
1917
+ message: `Key "${key}" must be an array.`,
1918
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1919
+ });
1920
+ return;
1921
+ }
1922
+ for (let i = 0; i < value.length; i++) {
1923
+ const element = value[i];
1924
+ if (element instanceof SfItem) {
1925
+ if (!isFiniteNumber(element.value)) issues.push({
1926
+ key,
1927
+ message: `Key "${key}" array element [${i}] must be a finite number.`,
1928
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1929
+ });
1930
+ } else if (!isFiniteNumber(element)) issues.push({
1931
+ key,
1932
+ message: `Key "${key}" array element [${i}] must be a finite number.`,
1933
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1934
+ });
1935
+ }
1936
+ }
1937
+ function validateStringArrayValue(key, value, issues) {
1938
+ if (!Array.isArray(value)) {
1939
+ issues.push({
1940
+ key,
1941
+ message: `Key "${key}" must be an array.`,
1942
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1943
+ });
1944
+ return;
1945
+ }
1946
+ for (let i = 0; i < value.length; i++) {
1947
+ const element = value[i];
1948
+ if (element instanceof SfItem) {
1949
+ if (typeof element.value !== "string") issues.push({
1950
+ key,
1951
+ message: `Key "${key}" array element [${i}] must be a string.`,
1952
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1953
+ });
1954
+ } else if (typeof element !== "string") issues.push({
1955
+ key,
1956
+ message: `Key "${key}" array element [${i}] must be a string.`,
1957
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1958
+ });
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Validates that all values in a CMCD payload conform to the expected types and constraints.
1963
+ *
1964
+ * @example
1965
+ * {@includeCode ../test/validateCmcdValues.test.ts#example}
1966
+ *
1967
+ * @param data - The CMCD payload to validate.
1968
+ * @param options - Validation options.
1969
+ * @returns The validation result.
1970
+ *
1971
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#reserved-keys | CTA-5004-A Reserved Keys}
1403
1972
  *
1404
1973
  * @public
1974
+ */
1975
+ function validateCmcdValues(data, options) {
1976
+ const version = resolveVersion(data, options);
1977
+ const issues = [];
1978
+ for (const [key, value] of Object.entries(data)) {
1979
+ if (isCmcdCustomKey(key)) {
1980
+ if (typeof value !== "string") issues.push({
1981
+ key,
1982
+ message: `Custom key "${key}" value must be a string or token.`,
1983
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1984
+ });
1985
+ else if (value.length > CMCD_CUSTOM_KEY_VALUE_MAX_LENGTH) issues.push({
1986
+ key,
1987
+ message: `Custom key "${key}" value exceeds maximum length of ${CMCD_CUSTOM_KEY_VALUE_MAX_LENGTH}.`,
1988
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1989
+ });
1990
+ continue;
1991
+ }
1992
+ if (key === "v") {
1993
+ if (value !== 1 && value !== 2) issues.push({
1994
+ key,
1995
+ message: `Key "v" must be 1 or 2.`,
1996
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1997
+ });
1998
+ continue;
1999
+ }
2000
+ let expectedType = CMCD_KEY_TYPES[key];
2001
+ if (!expectedType) continue;
2002
+ if (version === CMCD_V1 && key in CMCD_V1_KEY_TYPE_OVERRIDES) expectedType = CMCD_V1_KEY_TYPE_OVERRIDES[key];
2003
+ switch (expectedType) {
2004
+ case CMCD_KEY_TYPE_INTEGER:
2005
+ if (!isFiniteNumber(value) || !Number.isInteger(value)) issues.push({
2006
+ key,
2007
+ message: `Key "${key}" must be a finite integer.`,
2008
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2009
+ });
2010
+ else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
2011
+ key,
2012
+ message: `Key "${key}" should be rounded to the nearest 100.`,
2013
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
2014
+ });
2015
+ break;
2016
+ case CMCD_KEY_TYPE_NUMBER:
2017
+ if (!isFiniteNumber(value)) issues.push({
2018
+ key,
2019
+ message: `Key "${key}" must be a finite number.`,
2020
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2021
+ });
2022
+ else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
2023
+ key,
2024
+ message: `Key "${key}" should be rounded to the nearest 100.`,
2025
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
2026
+ });
2027
+ else if (INTEGER_ROUNDING_KEYS.has(key) && !Number.isInteger(value)) issues.push({
2028
+ key,
2029
+ message: `Key "${key}" should be rounded to an integer.`,
2030
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
2031
+ });
2032
+ break;
2033
+ case CMCD_KEY_TYPE_BOOLEAN:
2034
+ if (typeof value !== "boolean") issues.push({
2035
+ key,
2036
+ message: `Key "${key}" must be a boolean.`,
2037
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2038
+ });
2039
+ break;
2040
+ case CMCD_KEY_TYPE_STRING:
2041
+ if (typeof value !== "string") issues.push({
2042
+ key,
2043
+ message: `Key "${key}" must be a string.`,
2044
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2045
+ });
2046
+ else if (key in CMCD_STRING_LENGTH_LIMITS && value.length > CMCD_STRING_LENGTH_LIMITS[key]) issues.push({
2047
+ key,
2048
+ message: `Key "${key}" exceeds maximum length of ${CMCD_STRING_LENGTH_LIMITS[key]}.`,
2049
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2050
+ });
2051
+ break;
2052
+ case CMCD_KEY_TYPE_TOKEN: {
2053
+ const validValues = CMCD_TOKEN_VALUES[key];
2054
+ if (validValues && !validValues.includes(value)) issues.push({
2055
+ key,
2056
+ message: `Key "${key}" has invalid token value "${String(value)}". Expected one of: ${validValues.join(", ")}.`,
2057
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2058
+ });
2059
+ break;
2060
+ }
2061
+ case CMCD_KEY_TYPE_NUMBER_LIST:
2062
+ validateListValue(key, value, issues);
2063
+ break;
2064
+ case CMCD_KEY_TYPE_STRING_LIST:
2065
+ validateStringArrayValue(key, value, issues);
2066
+ break;
2067
+ }
2068
+ }
2069
+ return {
2070
+ valid: issues.every((i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR),
2071
+ issues
2072
+ };
2073
+ }
2074
+
2075
+ //#endregion
2076
+ //#region src/validateCmcd.ts
2077
+ /**
2078
+ * Validates a CMCD payload by checking keys, values, and structure.
1405
2079
  *
1406
2080
  * @example
1407
- * {@includeCode ../test/fromCmcdUrl.test.ts#example}
2081
+ * {@includeCode ../test/validateCmcd.test.ts#example}
2082
+ *
2083
+ * @param data - The CMCD payload to validate.
2084
+ * @param options - Validation options.
2085
+ * @returns The validation result.
2086
+ *
2087
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#data-payload-definition-what-data-to-send | CTA-5004-A Data Payload Definition}
2088
+ *
2089
+ * @public
1408
2090
  */
1409
- function fromCmcdUrl(url) {
1410
- return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")));
2091
+ function validateCmcd(data, options) {
2092
+ return mergeValidationResults(validateCmcdKeys(data, options), validateCmcdValues(data, options), validateCmcdStructure(data, options));
1411
2093
  }
1412
2094
 
1413
2095
  //#endregion
1414
- //#region src/toCmcdValue.ts
2096
+ //#region src/validateCmcdEvents.ts
1415
2097
  /**
1416
- * Convert a value to a CMCD value.
2098
+ * Validates a raw CMCD string as an event-mode payload.
1417
2099
  *
1418
- * @param value - The value to convert to a CMCD value.
1419
- * @param params - The parameters to convert to a CMCD value.
1420
- * @returns The CMCD value.
2100
+ * This function decodes the string internally and validates it with
2101
+ * `reportingMode` set to `'event'`. The input may contain multiple
2102
+ * newline-separated events (e.g. an `application/cmcd` POST body), in which
2103
+ * case each line is validated independently and the results are merged.
2104
+ *
2105
+ * @param cmcd - The raw CMCD-encoded string to validate. May contain
2106
+ * multiple newline-separated event lines.
2107
+ * @param options - Validation options (excluding `reportingMode`).
2108
+ * @returns The validation result including decoded data per event line.
2109
+ *
2110
+ * @example
2111
+ * {@includeCode ../test/validateCmcdEvents.test.ts#example}
2112
+ *
2113
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#body-definition | CTA-5004-A Body Definition}
1421
2114
  *
1422
2115
  * @public
1423
2116
  */
1424
- function toCmcdValue(value, params) {
1425
- return new SfItem(value, params);
2117
+ function validateCmcdEvents(cmcd, options) {
2118
+ const opts = {
2119
+ ...options,
2120
+ reportingMode: CMCD_EVENT_MODE
2121
+ };
2122
+ const lines = cmcd.split(/\r?\n/).filter((line) => line.length > 0);
2123
+ if (lines.length === 0) return {
2124
+ valid: false,
2125
+ issues: [{
2126
+ message: "Empty event mode payload. At least one event line is required.",
2127
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2128
+ }],
2129
+ data: []
2130
+ };
2131
+ const decodedLines = [];
2132
+ const lineResults = [];
2133
+ for (let i = 0; i < lines.length; i++) try {
2134
+ const data = decodeCmcd(lines[i]);
2135
+ decodedLines.push(data);
2136
+ lineResults.push(validateCmcd(data, opts));
2137
+ } catch {
2138
+ decodedLines.push({});
2139
+ lineResults.push({
2140
+ valid: false,
2141
+ issues: [{
2142
+ message: `Failed to decode event line ${i + 1}: invalid structured field syntax.`,
2143
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2144
+ }]
2145
+ });
2146
+ }
2147
+ return {
2148
+ ...mergeValidationResults(...lineResults),
2149
+ data: decodedLines
2150
+ };
2151
+ }
2152
+
2153
+ //#endregion
2154
+ //#region src/validateCmcdEventReport.ts
2155
+ /**
2156
+ * Validates a full HTTP request as an event-mode payload.
2157
+ *
2158
+ * Accepts an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
2159
+ *
2160
+ * This function validates that the request uses the POST method and has
2161
+ * the correct `Content-Type` header (`application/cmcd`) in addition to
2162
+ * validating the body content via {@link validateCmcdEvents}.
2163
+ *
2164
+ * @param request - An `HttpRequest` to validate.
2165
+ * @param options - Validation options (excluding `reportingMode`).
2166
+ * @returns The validation result including decoded data per event line.
2167
+ *
2168
+ * @example {@includeCode ../test/validateCmcdEventReport.test.ts#example}
2169
+ *
2170
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event-mode | CTA-5004-A Event Mode}
2171
+ *
2172
+ * @public
2173
+ */
2174
+ function validateCmcdEventReport(request, options) {
2175
+ const issues = [];
2176
+ if (request.method !== "POST") issues.push({
2177
+ message: `Invalid HTTP method '${request.method ?? "GET"}'. Event reports must use POST.`,
2178
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2179
+ });
2180
+ const contentType = request.headers ? ensureHeaders(request.headers).get("Content-Type") : void 0;
2181
+ if (!contentType) issues.push({
2182
+ message: `Missing Content-Type header. Event reports must use '${CMCD_MIME_TYPE}'.`,
2183
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2184
+ });
2185
+ else {
2186
+ const mediaType = contentType.split(";")[0].trim().toLowerCase();
2187
+ if (mediaType !== CMCD_MIME_TYPE) issues.push({
2188
+ message: `Invalid Content-Type '${mediaType}'. Event reports must use '${CMCD_MIME_TYPE}'.`,
2189
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2190
+ });
2191
+ }
2192
+ const body = request.body;
2193
+ if (body === void 0 || body === null) {
2194
+ issues.push({
2195
+ message: "Missing request body. Event reports must include a body.",
2196
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2197
+ });
2198
+ return {
2199
+ valid: false,
2200
+ issues,
2201
+ data: []
2202
+ };
2203
+ }
2204
+ if (typeof body !== "string") {
2205
+ issues.push({
2206
+ message: "Request body must be a string.",
2207
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2208
+ });
2209
+ return {
2210
+ valid: false,
2211
+ issues,
2212
+ data: []
2213
+ };
2214
+ }
2215
+ const bodyResult = validateCmcdEvents(body, options);
2216
+ return {
2217
+ valid: issues.length === 0 && bodyResult.valid,
2218
+ issues: [...issues, ...bodyResult.issues],
2219
+ data: bodyResult.data
2220
+ };
2221
+ }
2222
+
2223
+ //#endregion
2224
+ //#region src/validateCmcdHeaders.ts
2225
+ /**
2226
+ * Validates CMCD HTTP headers by checking shard placement and payload validity.
2227
+ *
2228
+ * This function accepts raw CMCD header strings, decodes each shard
2229
+ * internally, verifies that each key is placed in its correct header shard,
2230
+ * then merges all shards and runs full payload validation (keys, values, and
2231
+ * structure) on the merged data.
2232
+ *
2233
+ * @example
2234
+ * {@includeCode ../test/validateCmcdHeaders.test.ts#example}
2235
+ *
2236
+ * @param headers - A `Headers` instance or a record of CMCD header fields to their raw encoded string values.
2237
+ * @param options - Validation options (excluding `reportingMode`).
2238
+ * @returns The validation result including decoded data.
2239
+ *
2240
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#header-field-definition | CTA-5004-A Header Field Definition}
2241
+ *
2242
+ * @public
2243
+ */
2244
+ function validateCmcdHeaders(headers, options) {
2245
+ const h = ensureHeaders(headers);
2246
+ const issues = [];
2247
+ const decoded = {};
2248
+ for (const headerField of CMCD_HEADER_FIELDS) {
2249
+ const raw = h.get(headerField);
2250
+ if (!raw) continue;
2251
+ let shard;
2252
+ try {
2253
+ shard = decodeCmcd(raw);
2254
+ } catch {
2255
+ issues.push({
2256
+ key: headerField,
2257
+ message: `Failed to decode "${headerField}" header: invalid structured field syntax.`,
2258
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2259
+ });
2260
+ continue;
2261
+ }
2262
+ decoded[headerField] = shard;
2263
+ for (const key of Object.keys(shard)) {
2264
+ if (isCmcdCustomKey(key)) continue;
2265
+ const expectedHeader = CMCD_HEADER_MAP[key];
2266
+ if (expectedHeader && expectedHeader !== headerField) issues.push({
2267
+ key,
2268
+ message: `Key "${key}" is in "${headerField}" but should be in "${expectedHeader}".`,
2269
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2270
+ });
2271
+ }
2272
+ }
2273
+ const shardResult = {
2274
+ valid: issues.length === 0,
2275
+ issues
2276
+ };
2277
+ const merged = Object.assign({}, ...CMCD_HEADER_FIELDS.map((f) => decoded[f]).filter(Boolean));
2278
+ return {
2279
+ ...mergeValidationResults(shardResult, validateCmcd(merged, {
2280
+ ...options,
2281
+ reportingMode: CMCD_REQUEST_MODE
2282
+ })),
2283
+ data: merged
2284
+ };
2285
+ }
2286
+
2287
+ //#endregion
2288
+ //#region src/validateCmcdRequest.ts
2289
+ /**
2290
+ * Validates CMCD data from a request as a request-mode payload.
2291
+ *
2292
+ * Accepts a
2293
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request}
2294
+ * object or an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
2295
+ *
2296
+ * The function checks for CMCD data in the HTTP headers first. If CMCD
2297
+ * headers are found, validation includes shard-placement checks via
2298
+ * {@link validateCmcdHeaders}. Otherwise, the CMCD query parameter is
2299
+ * extracted from the URL and validated.
2300
+ *
2301
+ * @param request - A `Request` or `HttpRequest` to validate.
2302
+ * @param options - Validation options (excluding `reportingMode`).
2303
+ * @returns The validation result including decoded data.
2304
+ *
2305
+ * @example
2306
+ * {@includeCode ../test/validateCmcdRequest.test.ts#example}
2307
+ *
2308
+ * @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#request-mode | CTA-5004-A Request Mode}
2309
+ *
2310
+ * @public
2311
+ */
2312
+ function validateCmcdRequest(request, options) {
2313
+ const headers = extractHeaderRecord(request.headers);
2314
+ if (headers) return validateCmcdHeaders(headers, options);
2315
+ const param = new URL(request.url).searchParams.get(CMCD_PARAM);
2316
+ if (!param) return {
2317
+ valid: false,
2318
+ issues: [{
2319
+ message: "No CMCD data found in request headers or query parameters.",
2320
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2321
+ }],
2322
+ data: {}
2323
+ };
2324
+ let data;
2325
+ try {
2326
+ data = decodeCmcd(param);
2327
+ } catch {
2328
+ return {
2329
+ valid: false,
2330
+ issues: [{
2331
+ message: "Failed to decode CMCD query parameter: invalid structured field syntax.",
2332
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2333
+ }],
2334
+ data: {}
2335
+ };
2336
+ }
2337
+ return {
2338
+ ...validateCmcd(data, {
2339
+ ...options,
2340
+ reportingMode: CMCD_REQUEST_MODE
2341
+ }),
2342
+ data
2343
+ };
2344
+ }
2345
+ function extractHeaderRecord(headers) {
2346
+ if (!headers) return;
2347
+ const h = ensureHeaders(headers);
2348
+ const result = {};
2349
+ let found = false;
2350
+ for (const field of CMCD_HEADER_FIELDS) {
2351
+ const value = h.get(field);
2352
+ if (value) {
2353
+ result[field] = value;
2354
+ found = true;
2355
+ }
2356
+ }
2357
+ return found ? result : void 0;
1426
2358
  }
1427
2359
 
1428
2360
  //#endregion
1429
- export { CMCD_DEFAULT_TIME_INTERVAL, CMCD_EVENT_AD_BREAK_END, CMCD_EVENT_AD_BREAK_START, CMCD_EVENT_AD_END, CMCD_EVENT_AD_START, CMCD_EVENT_BACKGROUNDED_MODE, CMCD_EVENT_BITRATE_CHANGE, CMCD_EVENT_CONTENT_ID, CMCD_EVENT_CUSTOM_EVENT, CMCD_EVENT_ERROR, CMCD_EVENT_KEYS, CMCD_EVENT_MODE, CMCD_EVENT_MUTE, CMCD_EVENT_PLAYER_COLLAPSE, CMCD_EVENT_PLAYER_EXPAND, CMCD_EVENT_PLAY_STATE, CMCD_EVENT_RESPONSE_RECEIVED, CMCD_EVENT_SKIP, CMCD_EVENT_TIME_INTERVAL, CMCD_EVENT_UNMUTE, CMCD_FORMATTER_MAP, CMCD_HEADERS, CMCD_HEADER_MAP, CMCD_JSON, CMCD_KEYS, CMCD_OBJECT, CMCD_PARAM, CMCD_QUERY, CMCD_REQUEST, CMCD_REQUEST_KEYS, CMCD_REQUEST_MODE, CMCD_RESPONSE_KEYS, CMCD_SESSION, CMCD_STATUS, CMCD_V1, CMCD_V1_KEYS, CMCD_V2, CmcdEventType, CmcdHeaderField, CmcdObjectType, CmcdPlayerState, CmcdReporter, CmcdReportingMode, CmcdStreamType, CmcdStreamingFormat, CmcdTransmissionMode, appendCmcdHeaders, appendCmcdQuery, decodeCmcd, encodeCmcd, fromCmcdHeaders, fromCmcdQuery, fromCmcdUrl, groupCmcdHeaders, isCmcdCustomKey, isCmcdEventKey, isCmcdRequestKey, isCmcdResponseReceivedKey, isCmcdV1Key, prepareCmcdData, toCmcdHeaders, toCmcdQuery, toCmcdUrl, toCmcdValue };
2361
+ export { CMCD_DEFAULT_TIME_INTERVAL, CMCD_EVENT_AD_BREAK_END, CMCD_EVENT_AD_BREAK_START, CMCD_EVENT_AD_END, CMCD_EVENT_AD_START, CMCD_EVENT_BACKGROUNDED_MODE, CMCD_EVENT_BITRATE_CHANGE, CMCD_EVENT_CONTENT_ID, CMCD_EVENT_CUSTOM_EVENT, CMCD_EVENT_ERROR, CMCD_EVENT_KEYS, CMCD_EVENT_MODE, CMCD_EVENT_MUTE, CMCD_EVENT_PLAYER_COLLAPSE, CMCD_EVENT_PLAYER_EXPAND, CMCD_EVENT_PLAY_STATE, CMCD_EVENT_RESPONSE_RECEIVED, CMCD_EVENT_SKIP, CMCD_EVENT_TIME_INTERVAL, CMCD_EVENT_UNMUTE, CMCD_FORMATTER_MAP, CMCD_HEADERS, CMCD_HEADER_FIELDS, CMCD_HEADER_MAP, CMCD_JSON, CMCD_KEYS, CMCD_MIME_TYPE, CMCD_OBJECT, CMCD_PARAM, CMCD_QUERY, CMCD_REQUEST, CMCD_REQUEST_KEYS, CMCD_REQUEST_MODE, CMCD_RESPONSE_KEYS, CMCD_SESSION, CMCD_STATUS, CMCD_V1, CMCD_V1_KEYS, CMCD_V2, CMCD_VALIDATION_SEVERITY_ERROR, CMCD_VALIDATION_SEVERITY_WARNING, CmcdEventType, CmcdHeaderField, CmcdObjectType, CmcdPlayerState, CmcdReporter, CmcdReportingMode, CmcdStreamType, CmcdStreamingFormat, CmcdTransmissionMode, CmcdValidationSeverity, appendCmcdHeaders, appendCmcdQuery, decodeCmcd, encodeCmcd, fromCmcdHeaders, fromCmcdQuery, fromCmcdUrl, groupCmcdHeaders, isCmcdCustomKey, isCmcdEventKey, isCmcdRequestKey, isCmcdResponseReceivedKey, isCmcdV1Data, isCmcdV1Key, isCmcdV2Data, prepareCmcdData, toCmcdHeaders, toCmcdQuery, toCmcdUrl, toCmcdValue, validateCmcd, validateCmcdEventReport, validateCmcdEvents, validateCmcdHeaders, validateCmcdKeys, validateCmcdRequest, validateCmcdStructure, validateCmcdValues };
1430
2362
  //# sourceMappingURL=index.js.map