hls.js 1.5.12-0.canary.10340 → 1.5.12-0.canary.10343

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.
@@ -359,87 +359,6 @@ let ErrorDetails = /*#__PURE__*/function (ErrorDetails) {
359
359
  return ErrorDetails;
360
360
  }({});
361
361
 
362
- const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/;
363
- const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g;
364
-
365
- // adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
366
- class AttrList {
367
- constructor(attrs) {
368
- if (typeof attrs === 'string') {
369
- attrs = AttrList.parseAttrList(attrs);
370
- }
371
- _extends(this, attrs);
372
- }
373
- get clientAttrs() {
374
- return Object.keys(this).filter(attr => attr.substring(0, 2) === 'X-');
375
- }
376
- decimalInteger(attrName) {
377
- const intValue = parseInt(this[attrName], 10);
378
- if (intValue > Number.MAX_SAFE_INTEGER) {
379
- return Infinity;
380
- }
381
- return intValue;
382
- }
383
- hexadecimalInteger(attrName) {
384
- if (this[attrName]) {
385
- let stringValue = (this[attrName] || '0x').slice(2);
386
- stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
387
- const value = new Uint8Array(stringValue.length / 2);
388
- for (let i = 0; i < stringValue.length / 2; i++) {
389
- value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
390
- }
391
- return value;
392
- } else {
393
- return null;
394
- }
395
- }
396
- hexadecimalIntegerAsNumber(attrName) {
397
- const intValue = parseInt(this[attrName], 16);
398
- if (intValue > Number.MAX_SAFE_INTEGER) {
399
- return Infinity;
400
- }
401
- return intValue;
402
- }
403
- decimalFloatingPoint(attrName) {
404
- return parseFloat(this[attrName]);
405
- }
406
- optionalFloat(attrName, defaultValue) {
407
- const value = this[attrName];
408
- return value ? parseFloat(value) : defaultValue;
409
- }
410
- enumeratedString(attrName) {
411
- return this[attrName];
412
- }
413
- bool(attrName) {
414
- return this[attrName] === 'YES';
415
- }
416
- decimalResolution(attrName) {
417
- const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]);
418
- if (res === null) {
419
- return undefined;
420
- }
421
- return {
422
- width: parseInt(res[1], 10),
423
- height: parseInt(res[2], 10)
424
- };
425
- }
426
- static parseAttrList(input) {
427
- let match;
428
- const attrs = {};
429
- const quote = '"';
430
- ATTR_LIST_REGEX.lastIndex = 0;
431
- while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
432
- let value = match[2];
433
- if (value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1) {
434
- value = value.slice(1, -1);
435
- }
436
- const name = match[1].trim();
437
- attrs[name] = value;
438
- }
439
- return attrs;
440
- }
441
- }
442
-
443
362
  class Logger {
444
363
  constructor(label, logger) {
445
364
  this.trace = void 0;
@@ -501,7 +420,7 @@ function enableLogs(debugConfig, context, id) {
501
420
  // Some browsers don't allow to use bind on console object anyway
502
421
  // fallback to default if needed
503
422
  try {
504
- newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.5.12-0.canary.10340"}`);
423
+ newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.5.12-0.canary.10343"}`);
505
424
  } catch (e) {
506
425
  /* log fn threw an exception. All logger methods are no-ops. */
507
426
  return createLogger();
@@ -518,20 +437,169 @@ function enableLogs(debugConfig, context, id) {
518
437
  }
519
438
  const logger = exportedLogger;
520
439
 
440
+ const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/;
441
+ const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g;
442
+
443
+ // adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
444
+ class AttrList {
445
+ constructor(attrs, parsed) {
446
+ if (typeof attrs === 'string') {
447
+ attrs = AttrList.parseAttrList(attrs, parsed);
448
+ }
449
+ _extends(this, attrs);
450
+ }
451
+ get clientAttrs() {
452
+ return Object.keys(this).filter(attr => attr.substring(0, 2) === 'X-');
453
+ }
454
+ decimalInteger(attrName) {
455
+ const intValue = parseInt(this[attrName], 10);
456
+ if (intValue > Number.MAX_SAFE_INTEGER) {
457
+ return Infinity;
458
+ }
459
+ return intValue;
460
+ }
461
+ hexadecimalInteger(attrName) {
462
+ if (this[attrName]) {
463
+ let stringValue = (this[attrName] || '0x').slice(2);
464
+ stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
465
+ const value = new Uint8Array(stringValue.length / 2);
466
+ for (let i = 0; i < stringValue.length / 2; i++) {
467
+ value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
468
+ }
469
+ return value;
470
+ } else {
471
+ return null;
472
+ }
473
+ }
474
+ hexadecimalIntegerAsNumber(attrName) {
475
+ const intValue = parseInt(this[attrName], 16);
476
+ if (intValue > Number.MAX_SAFE_INTEGER) {
477
+ return Infinity;
478
+ }
479
+ return intValue;
480
+ }
481
+ decimalFloatingPoint(attrName) {
482
+ return parseFloat(this[attrName]);
483
+ }
484
+ optionalFloat(attrName, defaultValue) {
485
+ const value = this[attrName];
486
+ return value ? parseFloat(value) : defaultValue;
487
+ }
488
+ enumeratedString(attrName) {
489
+ return this[attrName];
490
+ }
491
+ enumeratedStringList(attrName, dict) {
492
+ const attrValue = this[attrName];
493
+ return (attrValue ? attrValue.split(/[ ,]+/) : []).reduce((result, identifier) => {
494
+ result[identifier.toLowerCase()] = true;
495
+ return result;
496
+ }, dict);
497
+ }
498
+ bool(attrName) {
499
+ return this[attrName] === 'YES';
500
+ }
501
+ decimalResolution(attrName) {
502
+ const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]);
503
+ if (res === null) {
504
+ return undefined;
505
+ }
506
+ return {
507
+ width: parseInt(res[1], 10),
508
+ height: parseInt(res[2], 10)
509
+ };
510
+ }
511
+ static parseAttrList(input, parsed) {
512
+ let match;
513
+ const attrs = {};
514
+ const quote = '"';
515
+ ATTR_LIST_REGEX.lastIndex = 0;
516
+ while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
517
+ const name = match[1].trim();
518
+ let value = match[2];
519
+ const quotedString = value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1;
520
+ let hexadecimalSequence = false;
521
+ if (quotedString) {
522
+ value = value.slice(1, -1);
523
+ } else {
524
+ switch (name) {
525
+ case 'IV':
526
+ case 'SCTE35-CMD':
527
+ case 'SCTE35-IN':
528
+ case 'SCTE35-OUT':
529
+ hexadecimalSequence = true;
530
+ }
531
+ }
532
+ if (parsed && (quotedString || hexadecimalSequence)) ; else if (!hexadecimalSequence && !quotedString) {
533
+ switch (name) {
534
+ case 'CLOSED-CAPTIONS':
535
+ if (value === 'NONE') {
536
+ break;
537
+ }
538
+ // falls through
539
+ case 'ALLOWED-CPC':
540
+ case 'CLASS':
541
+ case 'ASSOC-LANGUAGE':
542
+ case 'AUDIO':
543
+ case 'BYTERANGE':
544
+ case 'CHANNELS':
545
+ case 'CHARACTERISTICS':
546
+ case 'CODECS':
547
+ case 'DATA-ID':
548
+ case 'END-DATE':
549
+ case 'GROUP-ID':
550
+ case 'ID':
551
+ case 'IMPORT':
552
+ case 'INSTREAM-ID':
553
+ case 'KEYFORMAT':
554
+ case 'KEYFORMATVERSIONS':
555
+ case 'LANGUAGE':
556
+ case 'NAME':
557
+ case 'PATHWAY-ID':
558
+ case 'QUERYPARAM':
559
+ case 'RECENTLY-REMOVED-DATERANGES':
560
+ case 'SERVER-URI':
561
+ case 'STABLE-RENDITION-ID':
562
+ case 'STABLE-VARIANT-ID':
563
+ case 'START-DATE':
564
+ case 'SUBTITLES':
565
+ case 'SUPPLEMENTAL-CODECS':
566
+ case 'URI':
567
+ case 'VALUE':
568
+ case 'VIDEO':
569
+ case 'X-ASSET-LIST':
570
+ case 'X-ASSET-URI':
571
+ // Since we are not checking tag:attribute combination, just warn rather than ignoring attribute
572
+ logger.warn(`${input}: attribute ${name} is missing quotes`);
573
+ // continue;
574
+ }
575
+ }
576
+ attrs[name] = value;
577
+ }
578
+ return attrs;
579
+ }
580
+ }
581
+
521
582
  // Avoid exporting const enum so that these values can be inlined
522
583
 
584
+ const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial';
523
585
  function isDateRangeCueAttribute(attrName) {
524
- return attrName !== "ID" && attrName !== "CLASS" && attrName !== "START-DATE" && attrName !== "DURATION" && attrName !== "END-DATE" && attrName !== "END-ON-NEXT";
586
+ return attrName !== "ID" && attrName !== "CLASS" && attrName !== "CUE" && attrName !== "START-DATE" && attrName !== "DURATION" && attrName !== "END-DATE" && attrName !== "END-ON-NEXT";
525
587
  }
526
588
  function isSCTE35Attribute(attrName) {
527
- return attrName === "SCTE35-OUT" || attrName === "SCTE35-IN";
589
+ return attrName === "SCTE35-OUT" || attrName === "SCTE35-IN" || attrName === "SCTE35-CMD";
528
590
  }
529
591
  class DateRange {
530
- constructor(dateRangeAttr, dateRangeWithSameId) {
592
+ constructor(dateRangeAttr, dateRangeWithSameId, tagCount = 0) {
593
+ var _dateRangeWithSameId$;
531
594
  this.attr = void 0;
595
+ this.tagAnchor = void 0;
596
+ this.tagOrder = void 0;
532
597
  this._startDate = void 0;
533
598
  this._endDate = void 0;
599
+ this._cue = void 0;
534
600
  this._badValueForSameId = void 0;
601
+ this.tagAnchor = (dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.tagAnchor) || null;
602
+ this.tagOrder = (_dateRangeWithSameId$ = dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.tagOrder) != null ? _dateRangeWithSameId$ : tagCount;
535
603
  if (dateRangeWithSameId) {
536
604
  const previousAttr = dateRangeWithSameId.attr;
537
605
  for (const key in previousAttr) {
@@ -545,9 +613,9 @@ class DateRange {
545
613
  dateRangeAttr = _extends(new AttrList({}), previousAttr, dateRangeAttr);
546
614
  }
547
615
  this.attr = dateRangeAttr;
548
- this._startDate = new Date(dateRangeAttr["START-DATE"]);
616
+ this._startDate = dateRangeWithSameId ? dateRangeWithSameId.startDate : new Date(dateRangeAttr["START-DATE"]);
549
617
  if ("END-DATE" in this.attr) {
550
- const endDate = new Date(this.attr["END-DATE"]);
618
+ const endDate = (dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.endDate) || new Date(this.attr["END-DATE"]);
551
619
  if (isFiniteNumber(endDate.getTime())) {
552
620
  this._endDate = endDate;
553
621
  }
@@ -559,6 +627,28 @@ class DateRange {
559
627
  get class() {
560
628
  return this.attr.CLASS;
561
629
  }
630
+ get cue() {
631
+ const _cue = this._cue;
632
+ if (_cue === undefined) {
633
+ return this._cue = this.attr.enumeratedStringList(this.attr.CUE ? 'CUE' : 'X-CUE', {
634
+ pre: false,
635
+ post: false,
636
+ once: false
637
+ });
638
+ }
639
+ return _cue;
640
+ }
641
+ get startTime() {
642
+ const {
643
+ tagAnchor
644
+ } = this;
645
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
646
+ if (tagAnchor === null || tagAnchor.programDateTime === null) {
647
+ logger.warn(`Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`);
648
+ return NaN;
649
+ }
650
+ return tagAnchor.start + (this.startDate.getTime() - tagAnchor.programDateTime) / 1000;
651
+ }
562
652
  get startDate() {
563
653
  return this._startDate;
564
654
  }
@@ -592,8 +682,11 @@ class DateRange {
592
682
  get endOnNext() {
593
683
  return this.attr.bool("END-ON-NEXT");
594
684
  }
685
+ get isInterstitial() {
686
+ return this.class === CLASS_INTERSTITIAL;
687
+ }
595
688
  get isValid() {
596
- return !!this.id && !this._badValueForSameId && isFiniteNumber(this.startDate.getTime()) && (this.duration === null || this.duration >= 0) && (!this.endOnNext || !!this.class);
689
+ return !!this.id && !this._badValueForSameId && isFiniteNumber(this.startDate.getTime()) && (this.duration === null || this.duration >= 0) && (!this.endOnNext || !!this.class) && (!this.attr.CUE || !this.cue.pre && !this.cue.post || this.cue.pre !== this.cue.post) && (!this.isInterstitial || 'X-ASSET-URI' in this.attr || 'X-ASSET-LIST' in this.attr);
597
690
  }
598
691
  }
599
692
 
@@ -679,7 +772,6 @@ class BaseSegment {
679
772
  this._url = value;
680
773
  }
681
774
  }
682
-
683
775
  /**
684
776
  * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}.
685
777
  */
@@ -892,6 +984,7 @@ class LevelDetails {
892
984
  this.fragmentHint = void 0;
893
985
  this.partList = null;
894
986
  this.dateRanges = void 0;
987
+ this.dateRangeTagCount = 0;
895
988
  this.live = true;
896
989
  this.ageHeader = 0;
897
990
  this.advancedDateTime = void 0;
@@ -2485,7 +2578,7 @@ class M3U8Parser {
2485
2578
  if (result[1]) {
2486
2579
  var _level$unknownCodecs;
2487
2580
  // '#EXT-X-STREAM-INF' is found, parse level tag in group 1
2488
- const attrs = new AttrList(result[1]);
2581
+ const attrs = new AttrList(result[1], parsed);
2489
2582
  const uri = result[2];
2490
2583
  const level = {
2491
2584
  attrs,
@@ -2510,7 +2603,7 @@ class M3U8Parser {
2510
2603
  case 'SESSION-DATA':
2511
2604
  {
2512
2605
  // #EXT-X-SESSION-DATA
2513
- const sessionAttrs = new AttrList(attributes);
2606
+ const sessionAttrs = new AttrList(attributes, parsed);
2514
2607
  const dataId = sessionAttrs['DATA-ID'];
2515
2608
  if (dataId) {
2516
2609
  if (parsed.sessionData === null) {
@@ -2523,7 +2616,7 @@ class M3U8Parser {
2523
2616
  case 'SESSION-KEY':
2524
2617
  {
2525
2618
  // #EXT-X-SESSION-KEY
2526
- const sessionKey = parseKey(attributes, baseurl);
2619
+ const sessionKey = parseKey(attributes, baseurl, parsed);
2527
2620
  if (sessionKey.encrypted && sessionKey.isSupported()) {
2528
2621
  if (parsed.sessionKeys === null) {
2529
2622
  parsed.sessionKeys = [];
@@ -2541,7 +2634,7 @@ class M3U8Parser {
2541
2634
  case 'CONTENT-STEERING':
2542
2635
  {
2543
2636
  // #EXT-X-CONTENT-STEERING
2544
- const contentSteeringAttributes = new AttrList(attributes);
2637
+ const contentSteeringAttributes = new AttrList(attributes, parsed);
2545
2638
  parsed.contentSteering = {
2546
2639
  uri: M3U8Parser.resolve(contentSteeringAttributes['SERVER-URI'], baseurl),
2547
2640
  pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.'
@@ -2583,7 +2676,7 @@ class M3U8Parser {
2583
2676
  let id = 0;
2584
2677
  MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
2585
2678
  while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
2586
- const attrs = new AttrList(result[1]);
2679
+ const attrs = new AttrList(result[1], parsed);
2587
2680
  const type = attrs.TYPE;
2588
2681
  if (type) {
2589
2682
  const groups = groupsByType[type];
@@ -2635,6 +2728,7 @@ class M3U8Parser {
2635
2728
  static parseLevelPlaylist(string, baseurl, id, type, levelUrlId, multivariantVariableList) {
2636
2729
  const level = new LevelDetails(baseurl);
2637
2730
  const fragments = level.fragments;
2731
+ const programDateTimes = [];
2638
2732
  // The most recent init segment seen (applies to all subsequent segments)
2639
2733
  let currentInitSegment = null;
2640
2734
  let currentSN = 0;
@@ -2693,7 +2787,7 @@ class M3U8Parser {
2693
2787
  // avoid sliced strings https://github.com/video-dev/hls.js/issues/939
2694
2788
  const uri = (' ' + result[3]).slice(1);
2695
2789
  frag.relurl = uri;
2696
- assignProgramDateTime(frag, prevFrag);
2790
+ assignProgramDateTime(frag, prevFrag, programDateTimes);
2697
2791
  prevFrag = frag;
2698
2792
  totalduration += frag.duration;
2699
2793
  currentSN++;
@@ -2741,19 +2835,22 @@ class M3U8Parser {
2741
2835
  break;
2742
2836
  case 'SKIP':
2743
2837
  {
2744
- const skipAttrs = new AttrList(value1);
2838
+ if (level.skippedSegments) {
2839
+ level.playlistParsingError = new Error(`#EXT-X-SKIP MUST NOT appear more than once in a Playlist`);
2840
+ }
2841
+ const skipAttrs = new AttrList(value1, level);
2745
2842
  const skippedSegments = skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
2746
2843
  if (isFiniteNumber(skippedSegments)) {
2747
- level.skippedSegments = skippedSegments;
2844
+ level.skippedSegments += skippedSegments;
2748
2845
  // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
2749
2846
  for (let _i = skippedSegments; _i--;) {
2750
- fragments.unshift(null);
2847
+ fragments.push(null);
2751
2848
  }
2752
2849
  currentSN += skippedSegments;
2753
2850
  }
2754
2851
  const recentlyRemovedDateranges = skipAttrs.enumeratedString('RECENTLY-REMOVED-DATERANGES');
2755
2852
  if (recentlyRemovedDateranges) {
2756
- level.recentlyRemovedDateranges = recentlyRemovedDateranges.split('\t');
2853
+ level.recentlyRemovedDateranges = (level.recentlyRemovedDateranges || []).concat(recentlyRemovedDateranges.split('\t'));
2757
2854
  }
2758
2855
  break;
2759
2856
  }
@@ -2787,8 +2884,9 @@ class M3U8Parser {
2787
2884
  break;
2788
2885
  case 'DATERANGE':
2789
2886
  {
2790
- const dateRangeAttr = new AttrList(value1);
2791
- const dateRange = new DateRange(dateRangeAttr, level.dateRanges[dateRangeAttr.ID]);
2887
+ const dateRangeAttr = new AttrList(value1, level);
2888
+ const dateRange = new DateRange(dateRangeAttr, level.dateRanges[dateRangeAttr.ID], level.dateRangeTagCount);
2889
+ level.dateRangeTagCount++;
2792
2890
  if (dateRange.isValid || level.skippedSegments) {
2793
2891
  level.dateRanges[dateRange.id] = dateRange;
2794
2892
  } else {
@@ -2807,7 +2905,7 @@ class M3U8Parser {
2807
2905
  break;
2808
2906
  case 'KEY':
2809
2907
  {
2810
- const levelKey = parseKey(value1, baseurl);
2908
+ const levelKey = parseKey(value1, baseurl, level);
2811
2909
  if (levelKey.isSupported()) {
2812
2910
  if (levelKey.method === 'NONE') {
2813
2911
  levelkeys = undefined;
@@ -2830,7 +2928,7 @@ class M3U8Parser {
2830
2928
  break;
2831
2929
  case 'MAP':
2832
2930
  {
2833
- const mapAttrs = new AttrList(value1);
2931
+ const mapAttrs = new AttrList(value1, level);
2834
2932
  if (frag.duration) {
2835
2933
  // Initial segment tag is after segment duration tag.
2836
2934
  // #EXTINF: 6.0
@@ -2856,6 +2954,7 @@ class M3U8Parser {
2856
2954
  currentInitSegment = frag;
2857
2955
  createNextFrag = true;
2858
2956
  }
2957
+ currentInitSegment.cc = discontinuityCounter;
2859
2958
  break;
2860
2959
  }
2861
2960
  case 'SERVER-CONTROL':
@@ -2882,7 +2981,7 @@ class M3U8Parser {
2882
2981
  }
2883
2982
  const previousFragmentPart = currentPart > 0 ? partList[partList.length - 1] : undefined;
2884
2983
  const index = currentPart++;
2885
- const partAttrs = new AttrList(value1);
2984
+ const partAttrs = new AttrList(value1, level);
2886
2985
  const part = new Part(partAttrs, frag, baseurl, index, previousFragmentPart);
2887
2986
  partList.push(part);
2888
2987
  frag.duration += part.duration;
@@ -2890,13 +2989,13 @@ class M3U8Parser {
2890
2989
  }
2891
2990
  case 'PRELOAD-HINT':
2892
2991
  {
2893
- const preloadHintAttrs = new AttrList(value1);
2992
+ const preloadHintAttrs = new AttrList(value1, level);
2894
2993
  level.preloadHint = preloadHintAttrs;
2895
2994
  break;
2896
2995
  }
2897
2996
  case 'RENDITION-REPORT':
2898
2997
  {
2899
- const renditionReportAttrs = new AttrList(value1);
2998
+ const renditionReportAttrs = new AttrList(value1, level);
2900
2999
  level.renditionReports = level.renditionReports || [];
2901
3000
  level.renditionReports.push(renditionReportAttrs);
2902
3001
  break;
@@ -2914,7 +3013,7 @@ class M3U8Parser {
2914
3013
  level.fragmentHint = prevFrag;
2915
3014
  }
2916
3015
  } else if (level.partList) {
2917
- assignProgramDateTime(frag, prevFrag);
3016
+ assignProgramDateTime(frag, prevFrag, programDateTimes);
2918
3017
  frag.cc = discontinuityCounter;
2919
3018
  level.fragmentHint = frag;
2920
3019
  if (levelkeys) {
@@ -2935,6 +3034,21 @@ class M3U8Parser {
2935
3034
  if (firstFragment) {
2936
3035
  level.startCC = firstFragment.cc;
2937
3036
  }
3037
+ /**
3038
+ * Backfill any missing PDT values
3039
+ * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
3040
+ * one or more Media Segment URIs, the client SHOULD extrapolate
3041
+ * backward from that tag (using EXTINF durations and/or media
3042
+ * timestamps) to associate dates with those segments."
3043
+ * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
3044
+ * computed.
3045
+ */
3046
+ if (firstPdtIndex > 0) {
3047
+ backfillProgramDateTimes(fragments, firstPdtIndex);
3048
+ if (firstFragment) {
3049
+ programDateTimes.unshift(firstFragment);
3050
+ }
3051
+ }
2938
3052
  } else {
2939
3053
  level.endSN = 0;
2940
3054
  level.startCC = 0;
@@ -2943,27 +3057,62 @@ class M3U8Parser {
2943
3057
  totalduration += level.fragmentHint.duration;
2944
3058
  }
2945
3059
  level.totalduration = totalduration;
2946
- level.endCC = discontinuityCounter;
2947
-
2948
- /**
2949
- * Backfill any missing PDT values
2950
- * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
2951
- * one or more Media Segment URIs, the client SHOULD extrapolate
2952
- * backward from that tag (using EXTINF durations and/or media
2953
- * timestamps) to associate dates with those segments."
2954
- * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
2955
- * computed.
2956
- */
2957
- if (firstPdtIndex > 0) {
2958
- backfillProgramDateTimes(fragments, firstPdtIndex);
3060
+ if (programDateTimes.length && level.dateRangeTagCount && firstFragment) {
3061
+ mapDateRanges(programDateTimes, level);
2959
3062
  }
3063
+ level.endCC = discontinuityCounter;
2960
3064
  return level;
2961
3065
  }
2962
3066
  }
3067
+ function mapDateRanges(programDateTimes, details) {
3068
+ // Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
3069
+ const programDateTimeCount = programDateTimes.length;
3070
+ const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
3071
+ const playlistEnd = details.live ? Infinity : details.totalduration;
3072
+ const dateRangeIds = Object.keys(details.dateRanges);
3073
+ for (let i = dateRangeIds.length; i--;) {
3074
+ const dateRange = details.dateRanges[dateRangeIds[i]];
3075
+ const startDateTime = dateRange.startDate.getTime();
3076
+ dateRange.tagAnchor = lastProgramDateTime;
3077
+ for (let j = programDateTimeCount; j--;) {
3078
+ const fragIndex = findFragmentWithStartDate(details, startDateTime, programDateTimes, j, playlistEnd);
3079
+ if (fragIndex !== -1) {
3080
+ dateRange.tagAnchor = details.fragments[fragIndex];
3081
+ break;
3082
+ }
3083
+ }
3084
+ }
3085
+ }
3086
+ function findFragmentWithStartDate(details, startDateTime, programDateTimes, index, endTime) {
3087
+ const pdtFragment = programDateTimes[index];
3088
+ if (pdtFragment) {
3089
+ var _programDateTimes;
3090
+ // find matching range between PDT tags
3091
+ const durationBetweenPdt = (((_programDateTimes = programDateTimes[index + 1]) == null ? void 0 : _programDateTimes.start) || endTime) - pdtFragment.start;
3092
+ const pdtStart = pdtFragment.programDateTime;
3093
+ if ((startDateTime >= pdtStart || index === 0) && startDateTime <= pdtStart + durationBetweenPdt * 1000) {
3094
+ // map to fragment with date-time range
3095
+ const startIndex = programDateTimes[index].sn - details.startSN;
3096
+ const fragments = details.fragments;
3097
+ if (fragments.length > programDateTimes.length) {
3098
+ const endSegment = programDateTimes[index + 1] || fragments[fragments.length - 1];
3099
+ const endIndex = endSegment.sn - details.startSN;
3100
+ for (let i = endIndex; i > startIndex; i--) {
3101
+ const fragStartDateTime = fragments[i].programDateTime;
3102
+ if (startDateTime >= fragStartDateTime && startDateTime < fragStartDateTime + fragments[i].duration * 1000) {
3103
+ return i;
3104
+ }
3105
+ }
3106
+ }
3107
+ return startIndex;
3108
+ }
3109
+ }
3110
+ return -1;
3111
+ }
2963
3112
  function parseKey(keyTagAttributes, baseurl, parsed) {
2964
3113
  var _keyAttrs$METHOD, _keyAttrs$KEYFORMAT;
2965
3114
  // https://tools.ietf.org/html/rfc8216#section-4.3.2.4
2966
- const keyAttrs = new AttrList(keyTagAttributes);
3115
+ const keyAttrs = new AttrList(keyTagAttributes, parsed);
2967
3116
  const decryptmethod = (_keyAttrs$METHOD = keyAttrs.METHOD) != null ? _keyAttrs$METHOD : '';
2968
3117
  const decrypturi = keyAttrs.URI;
2969
3118
  const decryptiv = keyAttrs.hexadecimalInteger('IV');
@@ -3018,16 +3167,18 @@ function backfillProgramDateTimes(fragments, firstPdtIndex) {
3018
3167
  fragPrev = frag;
3019
3168
  }
3020
3169
  }
3021
- function assignProgramDateTime(frag, prevFrag) {
3170
+ function assignProgramDateTime(frag, prevFrag, programDateTimes) {
3022
3171
  if (frag.rawProgramDateTime) {
3023
3172
  frag.programDateTime = Date.parse(frag.rawProgramDateTime);
3173
+ if (!isFiniteNumber(frag.programDateTime)) {
3174
+ frag.programDateTime = null;
3175
+ frag.rawProgramDateTime = null;
3176
+ return;
3177
+ }
3178
+ programDateTimes.push(frag);
3024
3179
  } else if (prevFrag != null && prevFrag.programDateTime) {
3025
3180
  frag.programDateTime = prevFrag.endProgramDateTime;
3026
3181
  }
3027
- if (!isFiniteNumber(frag.programDateTime)) {
3028
- frag.programDateTime = null;
3029
- frag.rawProgramDateTime = null;
3030
- }
3031
3182
  }
3032
3183
  function setInitSegment(frag, mapAttrs, id, levelkeys) {
3033
3184
  frag.relurl = mapAttrs.URI;
@@ -4113,9 +4264,6 @@ const MAX_CUE_ENDTIME = (() => {
4113
4264
  }
4114
4265
  return Number.POSITIVE_INFINITY;
4115
4266
  })();
4116
- function dateRangeDateToTimelineSeconds(date, offset) {
4117
- return date.getTime() / 1000 - offset;
4118
- }
4119
4267
  function hexToArrayBuffer(str) {
4120
4268
  return Uint8Array.from(str.replace(/^0x/, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')).buffer;
4121
4269
  }
@@ -4146,6 +4294,7 @@ class ID3TrackController {
4146
4294
  hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
4147
4295
  hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
4148
4296
  hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
4297
+ hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
4149
4298
  }
4150
4299
  _unregisterListeners() {
4151
4300
  const {
@@ -4157,6 +4306,7 @@ class ID3TrackController {
4157
4306
  hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
4158
4307
  hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
4159
4308
  hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
4309
+ hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
4160
4310
  }
4161
4311
 
4162
4312
  // Add ID3 metatadata text track.
@@ -4297,6 +4447,14 @@ class ID3TrackController {
4297
4447
  onLevelUpdated(event, {
4298
4448
  details
4299
4449
  }) {
4450
+ this.updateDateRangeCues(details, true);
4451
+ }
4452
+ onLevelPtsUpdated(event, data) {
4453
+ if (Math.abs(data.drift) > 0.01) {
4454
+ this.updateDateRangeCues(data.details);
4455
+ }
4456
+ }
4457
+ updateDateRangeCues(details, removeOldCues) {
4300
4458
  if (!this.media || !details.hasProgramDateTime || !this.hls.config.enableDateRangeMetadataCues) {
4301
4459
  return;
4302
4460
  }
@@ -4309,7 +4467,7 @@ class ID3TrackController {
4309
4467
  } = details;
4310
4468
  const ids = Object.keys(dateRanges);
4311
4469
  // Remove cues from track not found in details.dateRanges
4312
- if (id3Track) {
4470
+ if (id3Track && removeOldCues) {
4313
4471
  const idsToRemove = Object.keys(dateRangeCuesAppended).filter(id => !ids.includes(id));
4314
4472
  for (let i = idsToRemove.length; i--;) {
4315
4473
  const id = idsToRemove[i];
@@ -4327,21 +4485,23 @@ class ID3TrackController {
4327
4485
  if (!this.id3Track) {
4328
4486
  this.id3Track = this.createTrack(this.media);
4329
4487
  }
4330
- const dateTimeOffset = lastFragment.programDateTime / 1000 - lastFragment.start;
4331
4488
  const Cue = getCueClass();
4332
4489
  for (let i = 0; i < ids.length; i++) {
4333
4490
  const id = ids[i];
4334
4491
  const dateRange = dateRanges[id];
4335
- const startTime = dateRangeDateToTimelineSeconds(dateRange.startDate, dateTimeOffset);
4492
+ const startTime = dateRange.startTime;
4336
4493
 
4337
4494
  // Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT)
4338
4495
  const appendedDateRangeCues = dateRangeCuesAppended[id];
4339
4496
  const cues = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.cues) || {};
4340
4497
  let durationKnown = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.durationKnown) || false;
4341
4498
  let endTime = MAX_CUE_ENDTIME;
4342
- const endDate = dateRange.endDate;
4343
- if (endDate) {
4344
- endTime = dateRangeDateToTimelineSeconds(endDate, dateTimeOffset);
4499
+ const {
4500
+ duration,
4501
+ endDate
4502
+ } = dateRange;
4503
+ if (endDate && duration !== null) {
4504
+ endTime = startTime + duration;
4345
4505
  durationKnown = true;
4346
4506
  } else if (dateRange.endOnNext && !durationKnown) {
4347
4507
  const nextDateRangeWithSameClass = ids.reduce((candidateDateRange, id) => {
@@ -4354,7 +4514,7 @@ class ID3TrackController {
4354
4514
  return candidateDateRange;
4355
4515
  }, null);
4356
4516
  if (nextDateRangeWithSameClass) {
4357
- endTime = dateRangeDateToTimelineSeconds(nextDateRangeWithSameClass.startDate, dateTimeOffset);
4517
+ endTime = nextDateRangeWithSameClass.startTime;
4358
4518
  durationKnown = true;
4359
4519
  }
4360
4520
  }
@@ -4371,6 +4531,9 @@ class ID3TrackController {
4371
4531
  if (cue) {
4372
4532
  if (durationKnown && !appendedDateRangeCues.durationKnown) {
4373
4533
  cue.endTime = endTime;
4534
+ } else if (Math.abs(cue.startTime - startTime) > 0.01) {
4535
+ cue.startTime = startTime;
4536
+ cue.endTime = endTime;
4374
4537
  }
4375
4538
  } else if (Cue) {
4376
4539
  let data = dateRange.attr[key];
@@ -4862,7 +5025,7 @@ function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS) {
4862
5025
  frag.endPTS = endPTS;
4863
5026
  frag.minEndPTS = minEndPTS;
4864
5027
  frag.endDTS = endDTS;
4865
- const sn = frag.sn; // 'initSegment'
5028
+ const sn = frag.sn;
4866
5029
  // exit if sn out of range
4867
5030
  if (!details || sn < details.startSN || sn > details.endSN) {
4868
5031
  return 0;
@@ -4940,8 +5103,8 @@ function mergeDetails(oldDetails, newDetails) {
4940
5103
  currentInitSegment = oldFrag.initSegment;
4941
5104
  }
4942
5105
  });
5106
+ const fragmentsToCheck = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments;
4943
5107
  if (currentInitSegment) {
4944
- const fragmentsToCheck = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments;
4945
5108
  fragmentsToCheck.forEach(frag => {
4946
5109
  var _currentInitSegment;
4947
5110
  if (frag && (!frag.initSegment || frag.initSegment.relurl === ((_currentInitSegment = currentInitSegment) == null ? void 0 : _currentInitSegment.relurl))) {
@@ -4958,8 +5121,19 @@ function mergeDetails(oldDetails, newDetails) {
4958
5121
  }
4959
5122
  newDetails.startSN = newDetails.fragments[0].sn;
4960
5123
  newDetails.startCC = newDetails.fragments[0].cc;
4961
- } else if (newDetails.canSkipDateRanges) {
4962
- newDetails.dateRanges = mergeDateRanges(oldDetails.dateRanges, newDetails.dateRanges, newDetails.recentlyRemovedDateranges);
5124
+ } else {
5125
+ if (newDetails.canSkipDateRanges) {
5126
+ newDetails.dateRanges = mergeDateRanges(oldDetails.dateRanges, newDetails);
5127
+ }
5128
+ const programDateTimes = oldDetails.fragments.filter(frag => frag.rawProgramDateTime);
5129
+ if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) {
5130
+ for (let i = 1; i < fragmentsToCheck.length; i++) {
5131
+ if (fragmentsToCheck[i].programDateTime === null) {
5132
+ assignProgramDateTime(fragmentsToCheck[i], fragmentsToCheck[i - 1], programDateTimes);
5133
+ }
5134
+ }
5135
+ }
5136
+ mapDateRanges(programDateTimes, newDetails);
4963
5137
  }
4964
5138
  }
4965
5139
  const newFragments = newDetails.fragments;
@@ -5008,21 +5182,33 @@ function mergeDetails(oldDetails, newDetails) {
5008
5182
  newDetails.advancedDateTime = oldDetails.advancedDateTime;
5009
5183
  }
5010
5184
  }
5011
- function mergeDateRanges(oldDateRanges, deltaDateRanges, recentlyRemovedDateranges) {
5185
+ function mergeDateRanges(oldDateRanges, newDetails) {
5186
+ const {
5187
+ dateRanges: deltaDateRanges,
5188
+ recentlyRemovedDateranges
5189
+ } = newDetails;
5012
5190
  const dateRanges = _extends({}, oldDateRanges);
5013
5191
  if (recentlyRemovedDateranges) {
5014
5192
  recentlyRemovedDateranges.forEach(id => {
5015
5193
  delete dateRanges[id];
5016
5194
  });
5017
5195
  }
5018
- Object.keys(deltaDateRanges).forEach(id => {
5019
- const dateRange = new DateRange(deltaDateRanges[id].attr, dateRanges[id]);
5020
- if (dateRange.isValid) {
5021
- dateRanges[id] = dateRange;
5022
- } else {
5023
- logger.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(deltaDateRanges[id].attr)}"`);
5024
- }
5025
- });
5196
+ const mergeIds = Object.keys(dateRanges);
5197
+ const mergeCount = mergeIds.length;
5198
+ if (mergeCount) {
5199
+ Object.keys(deltaDateRanges).forEach(id => {
5200
+ const mergedDateRange = dateRanges[id];
5201
+ const dateRange = new DateRange(deltaDateRanges[id].attr, mergedDateRange);
5202
+ if (dateRange.isValid) {
5203
+ dateRanges[id] = dateRange;
5204
+ if (!mergedDateRange) {
5205
+ dateRange.tagOrder += mergeCount;
5206
+ }
5207
+ } else {
5208
+ logger.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(deltaDateRanges[id].attr)}"`);
5209
+ }
5210
+ });
5211
+ }
5026
5212
  return dateRanges;
5027
5213
  }
5028
5214
  function mapPartIntersection(oldParts, newParts, intersectionFn) {
@@ -5049,7 +5235,7 @@ function mapFragmentIntersection(oldDetails, newDetails, intersectionFn) {
5049
5235
  for (let i = start; i <= end; i++) {
5050
5236
  const oldFrag = oldFrags[delta + i];
5051
5237
  let newFrag = newFrags[i];
5052
- if (skippedSegments && !newFrag && i < skippedSegments) {
5238
+ if (skippedSegments && !newFrag && oldFrag) {
5053
5239
  // Fill in skipped segments in delta playlist
5054
5240
  newFrag = newDetails.fragments[i] = oldFrag;
5055
5241
  }
@@ -5099,19 +5285,19 @@ function computeReloadInterval(newDetails, distanceToLiveEdgeMs = Infinity) {
5099
5285
  return Math.round(reloadInterval);
5100
5286
  }
5101
5287
  function getFragmentWithSN(level, sn, fragCurrent) {
5102
- if (!(level != null && level.details)) {
5288
+ const details = level == null ? void 0 : level.details;
5289
+ if (!details) {
5103
5290
  return null;
5104
5291
  }
5105
- const levelDetails = level.details;
5106
- let fragment = levelDetails.fragments[sn - levelDetails.startSN];
5292
+ let fragment = details.fragments[sn - details.startSN];
5107
5293
  if (fragment) {
5108
5294
  return fragment;
5109
5295
  }
5110
- fragment = levelDetails.fragmentHint;
5296
+ fragment = details.fragmentHint;
5111
5297
  if (fragment && fragment.sn === sn) {
5112
5298
  return fragment;
5113
5299
  }
5114
- if (sn < levelDetails.startSN && fragCurrent && fragCurrent.sn === sn) {
5300
+ if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) {
5115
5301
  return fragCurrent;
5116
5302
  }
5117
5303
  return null;
@@ -10588,13 +10774,10 @@ class FragmentTracker {
10588
10774
  */
10589
10775
  detectPartialFragments(data) {
10590
10776
  const timeRanges = this.timeRanges;
10591
- const {
10592
- frag,
10593
- part
10594
- } = data;
10595
- if (!timeRanges || frag.sn === 'initSegment') {
10777
+ if (!timeRanges || data.frag.sn === 'initSegment') {
10596
10778
  return;
10597
10779
  }
10780
+ const frag = data.frag;
10598
10781
  const fragKey = getFragmentKey(frag);
10599
10782
  const fragmentEntity = this.fragments[fragKey];
10600
10783
  if (!fragmentEntity || fragmentEntity.buffered && frag.gap) {
@@ -10608,7 +10791,7 @@ class FragmentTracker {
10608
10791
  }
10609
10792
  const timeRange = timeRanges[elementaryStream];
10610
10793
  const partial = isFragHint || streamInfo.partial === true;
10611
- fragmentEntity.range[elementaryStream] = this.getBufferedTimes(frag, part, partial, timeRange);
10794
+ fragmentEntity.range[elementaryStream] = this.getBufferedTimes(frag, data.part, partial, timeRange);
10612
10795
  });
10613
10796
  fragmentEntity.loaded = null;
10614
10797
  if (Object.keys(fragmentEntity.range).length) {
@@ -10761,18 +10944,14 @@ class FragmentTracker {
10761
10944
  return false;
10762
10945
  }
10763
10946
  onFragLoaded(event, data) {
10764
- const {
10765
- frag,
10766
- part
10767
- } = data;
10768
10947
  // don't track initsegment (for which sn is not a number)
10769
10948
  // don't track frags used for bitrateTest, they're irrelevant.
10770
- if (frag.sn === 'initSegment' || frag.bitrateTest) {
10949
+ if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) {
10771
10950
  return;
10772
10951
  }
10773
-
10952
+ const frag = data.frag;
10774
10953
  // Fragment entity `loaded` FragLoadedData is null when loading parts
10775
- const loaded = part ? null : data;
10954
+ const loaded = data.part ? null : data;
10776
10955
  const fragKey = getFragmentKey(frag);
10777
10956
  this.fragments[fragKey] = {
10778
10957
  body: frag,
@@ -12857,14 +13036,12 @@ class BaseStreamController extends TaskLoop {
12857
13036
  part.stats.parsing.end = now;
12858
13037
  }
12859
13038
  // See if part loading should be disabled/enabled based on buffer and playback position.
12860
- if (frag.sn !== 'initSegment') {
12861
- const levelDetails = this.getLevelDetails();
12862
- const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
12863
- const shouldLoadParts = loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
12864
- if (shouldLoadParts !== this.loadingParts) {
12865
- this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} after parsing segment ending @${frag.end.toFixed(2)}`);
12866
- this.loadingParts = shouldLoadParts;
12867
- }
13039
+ const levelDetails = this.getLevelDetails();
13040
+ const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
13041
+ const shouldLoadParts = loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
13042
+ if (shouldLoadParts !== this.loadingParts) {
13043
+ this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} after parsing segment ending @${frag.end.toFixed(2)}`);
13044
+ this.loadingParts = shouldLoadParts;
12868
13045
  }
12869
13046
  this.updateLevelTiming(frag, part, level, chunkMeta.partial);
12870
13047
  }
@@ -19738,8 +19915,8 @@ class StreamController extends BaseStreamController {
19738
19915
  }
19739
19916
  _handleFragmentLoadProgress(data) {
19740
19917
  var _frag$initSegment;
19918
+ const frag = data.frag;
19741
19919
  const {
19742
- frag,
19743
19920
  part,
19744
19921
  payload
19745
19922
  } = data;
@@ -20100,7 +20277,7 @@ class StreamController extends BaseStreamController {
20100
20277
  }
20101
20278
 
20102
20279
  // Avoid buffering if backtracking this fragment
20103
- if (video && details && frag.sn !== 'initSegment') {
20280
+ if (video && details) {
20104
20281
  const prevFrag = details.fragments[frag.sn - 1 - details.startSN];
20105
20282
  const isFirstFragment = frag.sn === details.startSN;
20106
20283
  const isFirstInDiscontinuity = !prevFrag || frag.cc > prevFrag.cc;
@@ -20386,7 +20563,7 @@ class Hls {
20386
20563
  * Get the video-dev/hls.js package version.
20387
20564
  */
20388
20565
  static get version() {
20389
- return "1.5.12-0.canary.10340";
20566
+ return "1.5.12-0.canary.10343";
20390
20567
  }
20391
20568
 
20392
20569
  /**