hls.js 1.5.12-0.canary.10338 → 1.5.12-0.canary.10341

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.
@@ -1,10 +1,12 @@
1
1
  import { AttrList } from '../utils/attr-list';
2
2
  import { logger } from '../utils/logger';
3
+ import type { Fragment } from './fragment';
3
4
 
4
5
  // Avoid exporting const enum so that these values can be inlined
5
6
  const enum DateRangeAttribute {
6
7
  ID = 'ID',
7
8
  CLASS = 'CLASS',
9
+ CUE = 'CUE',
8
10
  START_DATE = 'START-DATE',
9
11
  DURATION = 'DURATION',
10
12
  END_DATE = 'END-DATE',
@@ -12,12 +14,22 @@ const enum DateRangeAttribute {
12
14
  PLANNED_DURATION = 'PLANNED-DURATION',
13
15
  SCTE35_OUT = 'SCTE35-OUT',
14
16
  SCTE35_IN = 'SCTE35-IN',
17
+ SCTE35_CMD = 'SCTE35-CMD',
15
18
  }
16
19
 
20
+ export type DateRangeCue = {
21
+ pre: boolean;
22
+ post: boolean;
23
+ once: boolean;
24
+ };
25
+
26
+ const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial';
27
+
17
28
  export function isDateRangeCueAttribute(attrName: string): boolean {
18
29
  return (
19
30
  attrName !== DateRangeAttribute.ID &&
20
31
  attrName !== DateRangeAttribute.CLASS &&
32
+ attrName !== DateRangeAttribute.CUE &&
21
33
  attrName !== DateRangeAttribute.START_DATE &&
22
34
  attrName !== DateRangeAttribute.DURATION &&
23
35
  attrName !== DateRangeAttribute.END_DATE &&
@@ -28,17 +40,27 @@ export function isDateRangeCueAttribute(attrName: string): boolean {
28
40
  export function isSCTE35Attribute(attrName: string): boolean {
29
41
  return (
30
42
  attrName === DateRangeAttribute.SCTE35_OUT ||
31
- attrName === DateRangeAttribute.SCTE35_IN
43
+ attrName === DateRangeAttribute.SCTE35_IN ||
44
+ attrName === DateRangeAttribute.SCTE35_CMD
32
45
  );
33
46
  }
34
47
 
35
48
  export class DateRange {
36
49
  public attr: AttrList;
50
+ public tagAnchor: Fragment | null;
51
+ public tagOrder: number;
37
52
  private _startDate: Date;
38
53
  private _endDate?: Date;
54
+ private _cue?: DateRangeCue;
39
55
  private _badValueForSameId?: string;
40
56
 
41
- constructor(dateRangeAttr: AttrList, dateRangeWithSameId?: DateRange) {
57
+ constructor(
58
+ dateRangeAttr: AttrList,
59
+ dateRangeWithSameId?: DateRange | undefined,
60
+ tagCount: number = 0,
61
+ ) {
62
+ this.tagAnchor = dateRangeWithSameId?.tagAnchor || null;
63
+ this.tagOrder = dateRangeWithSameId?.tagOrder ?? tagCount;
42
64
  if (dateRangeWithSameId) {
43
65
  const previousAttr = dateRangeWithSameId.attr;
44
66
  for (const key in previousAttr) {
@@ -61,9 +83,13 @@ export class DateRange {
61
83
  );
62
84
  }
63
85
  this.attr = dateRangeAttr;
64
- this._startDate = new Date(dateRangeAttr[DateRangeAttribute.START_DATE]);
86
+ this._startDate = dateRangeWithSameId
87
+ ? dateRangeWithSameId.startDate
88
+ : new Date(dateRangeAttr[DateRangeAttribute.START_DATE]);
65
89
  if (DateRangeAttribute.END_DATE in this.attr) {
66
- const endDate = new Date(this.attr[DateRangeAttribute.END_DATE]);
90
+ const endDate =
91
+ dateRangeWithSameId?.endDate ||
92
+ new Date(this.attr[DateRangeAttribute.END_DATE]);
67
93
  if (Number.isFinite(endDate.getTime())) {
68
94
  this._endDate = endDate;
69
95
  }
@@ -78,6 +104,36 @@ export class DateRange {
78
104
  return this.attr.CLASS;
79
105
  }
80
106
 
107
+ get cue(): DateRangeCue {
108
+ const _cue = this._cue;
109
+ if (_cue === undefined) {
110
+ return (this._cue = this.attr.enumeratedStringList(
111
+ this.attr.CUE ? 'CUE' : 'X-CUE',
112
+ {
113
+ pre: false,
114
+ post: false,
115
+ once: false,
116
+ },
117
+ ));
118
+ }
119
+ return _cue;
120
+ }
121
+
122
+ get startTime(): number {
123
+ const { tagAnchor } = this;
124
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
125
+ if (tagAnchor === null || tagAnchor.programDateTime === null) {
126
+ logger.warn(
127
+ `Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`,
128
+ );
129
+ return NaN;
130
+ }
131
+ return (
132
+ tagAnchor.start +
133
+ (this.startDate.getTime() - tagAnchor.programDateTime) / 1000
134
+ );
135
+ }
136
+
81
137
  get startDate(): Date {
82
138
  return this._startDate;
83
139
  }
@@ -120,13 +176,23 @@ export class DateRange {
120
176
  return this.attr.bool(DateRangeAttribute.END_ON_NEXT);
121
177
  }
122
178
 
179
+ get isInterstitial(): boolean {
180
+ return this.class === CLASS_INTERSTITIAL;
181
+ }
182
+
123
183
  get isValid(): boolean {
124
184
  return (
125
185
  !!this.id &&
126
186
  !this._badValueForSameId &&
127
187
  Number.isFinite(this.startDate.getTime()) &&
128
188
  (this.duration === null || this.duration >= 0) &&
129
- (!this.endOnNext || !!this.class)
189
+ (!this.endOnNext || !!this.class) &&
190
+ (!this.attr.CUE ||
191
+ (!this.cue.pre && !this.cue.post) ||
192
+ this.cue.pre !== this.cue.post) &&
193
+ (!this.isInterstitial ||
194
+ 'X-ASSET-URI' in this.attr ||
195
+ 'X-ASSET-LIST' in this.attr)
130
196
  );
131
197
  }
132
198
  }
@@ -90,6 +90,10 @@ export class BaseSegment {
90
90
  }
91
91
  }
92
92
 
93
+ export interface MediaFragment extends Fragment {
94
+ sn: number;
95
+ }
96
+
93
97
  /**
94
98
  * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}.
95
99
  */
@@ -274,13 +278,13 @@ export class Part extends BaseSegment {
274
278
  public readonly gap: boolean = false;
275
279
  public readonly independent: boolean = false;
276
280
  public readonly relurl: string;
277
- public readonly fragment: Fragment;
281
+ public readonly fragment: MediaFragment;
278
282
  public readonly index: number;
279
283
  public stats: LoadStats = new LoadStats();
280
284
 
281
285
  constructor(
282
286
  partAttrs: AttrList,
283
- frag: Fragment,
287
+ frag: MediaFragment,
284
288
  baseurl: string,
285
289
  index: number,
286
290
  previous?: Part,
@@ -1,7 +1,7 @@
1
- import { Part } from './fragment';
2
- import type { Fragment } from './fragment';
3
- import type { AttrList } from '../utils/attr-list';
1
+ import type { Part } from './fragment';
4
2
  import type { DateRange } from './date-range';
3
+ import type { Fragment, MediaFragment } from './fragment';
4
+ import type { AttrList } from '../utils/attr-list';
5
5
  import type { VariableMap } from '../types/level';
6
6
 
7
7
  const DEFAULT_TARGET_DURATION = 10;
@@ -15,10 +15,11 @@ export class LevelDetails {
15
15
  public averagetargetduration?: number;
16
16
  public endCC: number = 0;
17
17
  public endSN: number = 0;
18
- public fragments: Fragment[];
19
- public fragmentHint?: Fragment;
18
+ public fragments: MediaFragment[];
19
+ public fragmentHint?: MediaFragment;
20
20
  public partList: Part[] | null = null;
21
21
  public dateRanges: Record<string, DateRange>;
22
+ public dateRangeTagCount: number = 0;
22
23
  public live: boolean = true;
23
24
  public ageHeader: number = 0;
24
25
  public advancedDateTime?: number;
@@ -148,7 +149,7 @@ export class LevelDetails {
148
149
 
149
150
  get lastPartSn(): number {
150
151
  if (this.partList?.length) {
151
- return this.partList[this.partList.length - 1].fragment.sn as number;
152
+ return this.partList[this.partList.length - 1].fragment.sn;
152
153
  }
153
154
  return this.endSN;
154
155
  }
@@ -1,6 +1,6 @@
1
1
  import { buildAbsoluteURL } from 'url-toolkit';
2
2
  import { DateRange } from './date-range';
3
- import { Fragment, Part } from './fragment';
3
+ import { Fragment, MediaFragment, Part } from './fragment';
4
4
  import { LevelDetails } from './level-details';
5
5
  import { LevelKey } from './level-key';
6
6
  import { AttrList } from '../utils/attr-list';
@@ -10,7 +10,6 @@ import {
10
10
  hasVariableReferences,
11
11
  importVariableDefinition,
12
12
  substituteVariables,
13
- substituteVariablesInAttributes,
14
13
  } from '../utils/variable-substitution';
15
14
  import { isCodecType } from '../utils/codecs';
16
15
  import type { CodecType } from '../utils/codecs';
@@ -120,21 +119,7 @@ export default class M3U8Parser {
120
119
  while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
121
120
  if (result[1]) {
122
121
  // '#EXT-X-STREAM-INF' is found, parse level tag in group 1
123
- const attrs = new AttrList(result[1]) as LevelAttributes;
124
- if (__USE_VARIABLE_SUBSTITUTION__) {
125
- substituteVariablesInAttributes(parsed, attrs, [
126
- 'CODECS',
127
- 'SUPPLEMENTAL-CODECS',
128
- 'ALLOWED-CPC',
129
- 'PATHWAY-ID',
130
- 'STABLE-VARIANT-ID',
131
- 'AUDIO',
132
- 'VIDEO',
133
- 'SUBTITLES',
134
- 'CLOSED-CAPTIONS',
135
- 'NAME',
136
- ]);
137
- }
122
+ const attrs = new AttrList(result[1], parsed) as LevelAttributes;
138
123
  const uri = __USE_VARIABLE_SUBSTITUTION__
139
124
  ? substituteVariables(parsed, result[2])
140
125
  : result[2];
@@ -166,15 +151,7 @@ export default class M3U8Parser {
166
151
  switch (tag) {
167
152
  case 'SESSION-DATA': {
168
153
  // #EXT-X-SESSION-DATA
169
- const sessionAttrs = new AttrList(attributes);
170
- if (__USE_VARIABLE_SUBSTITUTION__) {
171
- substituteVariablesInAttributes(parsed, sessionAttrs, [
172
- 'DATA-ID',
173
- 'LANGUAGE',
174
- 'VALUE',
175
- 'URI',
176
- ]);
177
- }
154
+ const sessionAttrs = new AttrList(attributes, parsed);
178
155
  const dataId = sessionAttrs['DATA-ID'];
179
156
  if (dataId) {
180
157
  if (parsed.sessionData === null) {
@@ -202,26 +179,14 @@ export default class M3U8Parser {
202
179
  case 'DEFINE': {
203
180
  // #EXT-X-DEFINE
204
181
  if (__USE_VARIABLE_SUBSTITUTION__) {
205
- const variableAttributes = new AttrList(attributes);
206
- substituteVariablesInAttributes(parsed, variableAttributes, [
207
- 'NAME',
208
- 'VALUE',
209
- 'QUERYPARAM',
210
- ]);
182
+ const variableAttributes = new AttrList(attributes, parsed);
211
183
  addVariableDefinition(parsed, variableAttributes, baseurl);
212
184
  }
213
185
  break;
214
186
  }
215
187
  case 'CONTENT-STEERING': {
216
188
  // #EXT-X-CONTENT-STEERING
217
- const contentSteeringAttributes = new AttrList(attributes);
218
- if (__USE_VARIABLE_SUBSTITUTION__) {
219
- substituteVariablesInAttributes(
220
- parsed,
221
- contentSteeringAttributes,
222
- ['SERVER-URI', 'PATHWAY-ID'],
223
- );
224
- }
189
+ const contentSteeringAttributes = new AttrList(attributes, parsed);
225
190
  parsed.contentSteering = {
226
191
  uri: M3U8Parser.resolve(
227
192
  contentSteeringAttributes['SERVER-URI'],
@@ -278,26 +243,13 @@ export default class M3U8Parser {
278
243
  let id = 0;
279
244
  MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
280
245
  while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
281
- const attrs = new AttrList(result[1]) as MediaAttributes;
246
+ const attrs = new AttrList(result[1], parsed) as MediaAttributes;
282
247
  const type = attrs.TYPE;
283
248
  if (type) {
284
249
  const groups: (typeof groupsByType)[keyof typeof groupsByType] =
285
250
  groupsByType[type];
286
251
  const medias: MediaPlaylist[] = results[type] || [];
287
252
  results[type] = medias;
288
- if (__USE_VARIABLE_SUBSTITUTION__) {
289
- substituteVariablesInAttributes(parsed, attrs, [
290
- 'URI',
291
- 'GROUP-ID',
292
- 'LANGUAGE',
293
- 'ASSOC-LANGUAGE',
294
- 'STABLE-RENDITION-ID',
295
- 'NAME',
296
- 'INSTREAM-ID',
297
- 'CHARACTERISTICS',
298
- 'CHANNELS',
299
- ]);
300
- }
301
253
  const lang = attrs.LANGUAGE;
302
254
  const assocLang = attrs['ASSOC-LANGUAGE'];
303
255
  const channels = attrs.CHANNELS;
@@ -355,6 +307,7 @@ export default class M3U8Parser {
355
307
  ): LevelDetails {
356
308
  const level = new LevelDetails(baseurl);
357
309
  const fragments: M3U8ParserFragments = level.fragments;
310
+ const programDateTimes: MediaFragment[] = [];
358
311
  // The most recent init segment seen (applies to all subsequent segments)
359
312
  let currentInitSegment: Fragment | null = null;
360
313
  let currentSN = 0;
@@ -420,7 +373,11 @@ export default class M3U8Parser {
420
373
  frag.relurl = __USE_VARIABLE_SUBSTITUTION__
421
374
  ? substituteVariables(level, uri)
422
375
  : uri;
423
- assignProgramDateTime(frag, prevFrag);
376
+ assignProgramDateTime(
377
+ frag as MediaFragment,
378
+ prevFrag as MediaFragment,
379
+ programDateTimes,
380
+ );
424
381
  prevFrag = frag;
425
382
  totalduration += frag.duration;
426
383
  currentSN++;
@@ -468,19 +425,19 @@ export default class M3U8Parser {
468
425
  currentSN = level.startSN = parseInt(value1);
469
426
  break;
470
427
  case 'SKIP': {
471
- const skipAttrs = new AttrList(value1);
472
- if (__USE_VARIABLE_SUBSTITUTION__) {
473
- substituteVariablesInAttributes(level, skipAttrs, [
474
- 'RECENTLY-REMOVED-DATERANGES',
475
- ]);
428
+ if (level.skippedSegments) {
429
+ level.playlistParsingError = new Error(
430
+ `#EXT-X-SKIP MUST NOT appear more than once in a Playlist`,
431
+ );
476
432
  }
433
+ const skipAttrs = new AttrList(value1, level);
477
434
  const skippedSegments =
478
435
  skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
479
436
  if (Number.isFinite(skippedSegments)) {
480
- level.skippedSegments = skippedSegments;
437
+ level.skippedSegments += skippedSegments;
481
438
  // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
482
439
  for (let i = skippedSegments; i--; ) {
483
- fragments.unshift(null);
440
+ fragments.push(null);
484
441
  }
485
442
  currentSN += skippedSegments;
486
443
  }
@@ -488,8 +445,9 @@ export default class M3U8Parser {
488
445
  'RECENTLY-REMOVED-DATERANGES',
489
446
  );
490
447
  if (recentlyRemovedDateranges) {
491
- level.recentlyRemovedDateranges =
492
- recentlyRemovedDateranges.split('\t');
448
+ level.recentlyRemovedDateranges = (
449
+ level.recentlyRemovedDateranges || []
450
+ ).concat(recentlyRemovedDateranges.split('\t'));
493
451
  }
494
452
  break;
495
453
  }
@@ -522,27 +480,13 @@ export default class M3U8Parser {
522
480
  frag.tagList.push([tag, value1]);
523
481
  break;
524
482
  case 'DATERANGE': {
525
- const dateRangeAttr = new AttrList(value1);
526
- if (__USE_VARIABLE_SUBSTITUTION__) {
527
- substituteVariablesInAttributes(level, dateRangeAttr, [
528
- 'ID',
529
- 'CLASS',
530
- 'START-DATE',
531
- 'END-DATE',
532
- 'SCTE35-CMD',
533
- 'SCTE35-OUT',
534
- 'SCTE35-IN',
535
- ]);
536
- substituteVariablesInAttributes(
537
- level,
538
- dateRangeAttr,
539
- dateRangeAttr.clientAttrs,
540
- );
541
- }
483
+ const dateRangeAttr = new AttrList(value1, level);
542
484
  const dateRange = new DateRange(
543
485
  dateRangeAttr,
544
486
  level.dateRanges[dateRangeAttr.ID],
487
+ level.dateRangeTagCount,
545
488
  );
489
+ level.dateRangeTagCount++;
546
490
  if (dateRange.isValid || level.skippedSegments) {
547
491
  level.dateRanges[dateRange.id] = dateRange;
548
492
  } else {
@@ -554,13 +498,7 @@ export default class M3U8Parser {
554
498
  }
555
499
  case 'DEFINE': {
556
500
  if (__USE_VARIABLE_SUBSTITUTION__) {
557
- const variableAttributes = new AttrList(value1);
558
- substituteVariablesInAttributes(level, variableAttributes, [
559
- 'NAME',
560
- 'VALUE',
561
- 'IMPORT',
562
- 'QUERYPARAM',
563
- ]);
501
+ const variableAttributes = new AttrList(value1, level);
564
502
  if ('IMPORT' in variableAttributes) {
565
503
  importVariableDefinition(
566
504
  level,
@@ -600,13 +538,7 @@ export default class M3U8Parser {
600
538
  level.startTimeOffset = parseStartTimeOffset(value1);
601
539
  break;
602
540
  case 'MAP': {
603
- const mapAttrs = new AttrList(value1);
604
- if (__USE_VARIABLE_SUBSTITUTION__) {
605
- substituteVariablesInAttributes(level, mapAttrs, [
606
- 'BYTERANGE',
607
- 'URI',
608
- ]);
609
- }
541
+ const mapAttrs = new AttrList(value1, level);
610
542
  if (frag.duration) {
611
543
  // Initial segment tag is after segment duration tag.
612
544
  // #EXTINF: 6.0
@@ -635,6 +567,7 @@ export default class M3U8Parser {
635
567
  currentInitSegment = frag;
636
568
  createNextFrag = true;
637
569
  }
570
+ currentInitSegment.cc = discontinuityCounter;
638
571
  break;
639
572
  }
640
573
  case 'SERVER-CONTROL': {
@@ -667,16 +600,10 @@ export default class M3U8Parser {
667
600
  const previousFragmentPart =
668
601
  currentPart > 0 ? partList[partList.length - 1] : undefined;
669
602
  const index = currentPart++;
670
- const partAttrs = new AttrList(value1);
671
- if (__USE_VARIABLE_SUBSTITUTION__) {
672
- substituteVariablesInAttributes(level, partAttrs, [
673
- 'BYTERANGE',
674
- 'URI',
675
- ]);
676
- }
603
+ const partAttrs = new AttrList(value1, level);
677
604
  const part = new Part(
678
605
  partAttrs,
679
- frag,
606
+ frag as MediaFragment,
680
607
  baseurl,
681
608
  index,
682
609
  previousFragmentPart,
@@ -686,20 +613,12 @@ export default class M3U8Parser {
686
613
  break;
687
614
  }
688
615
  case 'PRELOAD-HINT': {
689
- const preloadHintAttrs = new AttrList(value1);
690
- if (__USE_VARIABLE_SUBSTITUTION__) {
691
- substituteVariablesInAttributes(level, preloadHintAttrs, ['URI']);
692
- }
616
+ const preloadHintAttrs = new AttrList(value1, level);
693
617
  level.preloadHint = preloadHintAttrs;
694
618
  break;
695
619
  }
696
620
  case 'RENDITION-REPORT': {
697
- const renditionReportAttrs = new AttrList(value1);
698
- if (__USE_VARIABLE_SUBSTITUTION__) {
699
- substituteVariablesInAttributes(level, renditionReportAttrs, [
700
- 'URI',
701
- ]);
702
- }
621
+ const renditionReportAttrs = new AttrList(value1, level);
703
622
  level.renditionReports = level.renditionReports || [];
704
623
  level.renditionReports.push(renditionReportAttrs);
705
624
  break;
@@ -714,12 +633,16 @@ export default class M3U8Parser {
714
633
  fragments.pop();
715
634
  totalduration -= prevFrag.duration;
716
635
  if (level.partList) {
717
- level.fragmentHint = prevFrag;
636
+ level.fragmentHint = prevFrag as MediaFragment;
718
637
  }
719
638
  } else if (level.partList) {
720
- assignProgramDateTime(frag, prevFrag);
639
+ assignProgramDateTime(
640
+ frag as MediaFragment,
641
+ prevFrag as MediaFragment,
642
+ programDateTimes,
643
+ );
721
644
  frag.cc = discontinuityCounter;
722
- level.fragmentHint = frag;
645
+ level.fragmentHint = frag as MediaFragment;
723
646
  if (levelkeys) {
724
647
  setFragLevelKeys(frag, levelkeys, level);
725
648
  }
@@ -738,6 +661,21 @@ export default class M3U8Parser {
738
661
  if (firstFragment) {
739
662
  level.startCC = firstFragment.cc;
740
663
  }
664
+ /**
665
+ * Backfill any missing PDT values
666
+ * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
667
+ * one or more Media Segment URIs, the client SHOULD extrapolate
668
+ * backward from that tag (using EXTINF durations and/or media
669
+ * timestamps) to associate dates with those segments."
670
+ * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
671
+ * computed.
672
+ */
673
+ if (firstPdtIndex > 0) {
674
+ backfillProgramDateTimes(fragments, firstPdtIndex);
675
+ if (firstFragment) {
676
+ programDateTimes.unshift(firstFragment as MediaFragment);
677
+ }
678
+ }
741
679
  } else {
742
680
  level.endSN = 0;
743
681
  level.startCC = 0;
@@ -746,23 +684,83 @@ export default class M3U8Parser {
746
684
  totalduration += level.fragmentHint.duration;
747
685
  }
748
686
  level.totalduration = totalduration;
687
+ if (programDateTimes.length && level.dateRangeTagCount && firstFragment) {
688
+ mapDateRanges(programDateTimes, level);
689
+ }
690
+
749
691
  level.endCC = discontinuityCounter;
750
692
 
751
- /**
752
- * Backfill any missing PDT values
753
- * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
754
- * one or more Media Segment URIs, the client SHOULD extrapolate
755
- * backward from that tag (using EXTINF durations and/or media
756
- * timestamps) to associate dates with those segments."
757
- * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
758
- * computed.
759
- */
760
- if (firstPdtIndex > 0) {
761
- backfillProgramDateTimes(fragments, firstPdtIndex);
693
+ return level;
694
+ }
695
+ }
696
+
697
+ export function mapDateRanges(
698
+ programDateTimes: MediaFragment[],
699
+ details: LevelDetails,
700
+ ) {
701
+ // Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
702
+ const programDateTimeCount = programDateTimes.length;
703
+ const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
704
+ const playlistEnd = details.live ? Infinity : details.totalduration;
705
+ const dateRangeIds = Object.keys(details.dateRanges);
706
+ for (let i = dateRangeIds.length; i--; ) {
707
+ const dateRange = details.dateRanges[dateRangeIds[i]];
708
+ const startDateTime = dateRange.startDate.getTime();
709
+ dateRange.tagAnchor = lastProgramDateTime;
710
+ for (let j = programDateTimeCount; j--; ) {
711
+ const fragIndex = findFragmentWithStartDate(
712
+ details,
713
+ startDateTime,
714
+ programDateTimes,
715
+ j,
716
+ playlistEnd,
717
+ );
718
+ if (fragIndex !== -1) {
719
+ dateRange.tagAnchor = details.fragments[fragIndex];
720
+ break;
721
+ }
762
722
  }
723
+ }
724
+ }
763
725
 
764
- return level;
726
+ function findFragmentWithStartDate(
727
+ details: LevelDetails,
728
+ startDateTime: number,
729
+ programDateTimes: MediaFragment[],
730
+ index: number,
731
+ endTime: number,
732
+ ): number {
733
+ const pdtFragment = programDateTimes[index];
734
+ if (pdtFragment) {
735
+ // find matching range between PDT tags
736
+ const durationBetweenPdt =
737
+ (programDateTimes[index + 1]?.start || endTime) - pdtFragment.start;
738
+ const pdtStart = pdtFragment.programDateTime as number;
739
+ if (
740
+ (startDateTime >= pdtStart || index === 0) &&
741
+ startDateTime <= pdtStart + durationBetweenPdt * 1000
742
+ ) {
743
+ // map to fragment with date-time range
744
+ const startIndex = programDateTimes[index].sn - details.startSN;
745
+ const fragments = details.fragments;
746
+ if (fragments.length > programDateTimes.length) {
747
+ const endSegment =
748
+ programDateTimes[index + 1] || fragments[fragments.length - 1];
749
+ const endIndex = endSegment.sn - details.startSN;
750
+ for (let i = endIndex; i > startIndex; i--) {
751
+ const fragStartDateTime = fragments[i].programDateTime as number;
752
+ if (
753
+ startDateTime >= fragStartDateTime &&
754
+ startDateTime < fragStartDateTime + fragments[i].duration * 1000
755
+ ) {
756
+ return i;
757
+ }
758
+ }
759
+ }
760
+ return startIndex;
761
+ }
765
762
  }
763
+ return -1;
766
764
  }
767
765
 
768
766
  function parseKey(
@@ -771,16 +769,7 @@ function parseKey(
771
769
  parsed: ParsedMultivariantPlaylist | LevelDetails,
772
770
  ): LevelKey {
773
771
  // https://tools.ietf.org/html/rfc8216#section-4.3.2.4
774
- const keyAttrs = new AttrList(keyTagAttributes);
775
- if (__USE_VARIABLE_SUBSTITUTION__) {
776
- substituteVariablesInAttributes(parsed, keyAttrs, [
777
- 'KEYFORMAT',
778
- 'KEYFORMATVERSIONS',
779
- 'URI',
780
- 'IV',
781
- 'URI',
782
- ]);
783
- }
772
+ const keyAttrs = new AttrList(keyTagAttributes, parsed);
784
773
  const decryptmethod = keyAttrs.METHOD ?? '';
785
774
  const decrypturi = keyAttrs.URI;
786
775
  const decryptiv = keyAttrs.hexadecimalInteger('IV');
@@ -864,17 +853,22 @@ function backfillProgramDateTimes(
864
853
  }
865
854
  }
866
855
 
867
- function assignProgramDateTime(frag, prevFrag) {
856
+ export function assignProgramDateTime(
857
+ frag: MediaFragment,
858
+ prevFrag: MediaFragment | null,
859
+ programDateTimes: MediaFragment[],
860
+ ) {
868
861
  if (frag.rawProgramDateTime) {
869
862
  frag.programDateTime = Date.parse(frag.rawProgramDateTime);
863
+ if (!Number.isFinite(frag.programDateTime)) {
864
+ frag.programDateTime = null;
865
+ frag.rawProgramDateTime = null;
866
+ return;
867
+ }
868
+ programDateTimes.push(frag);
870
869
  } else if (prevFrag?.programDateTime) {
871
870
  frag.programDateTime = prevFrag.endProgramDateTime;
872
871
  }
873
-
874
- if (!Number.isFinite(frag.programDateTime)) {
875
- frag.programDateTime = null;
876
- frag.rawProgramDateTime = null;
877
- }
878
872
  }
879
873
 
880
874
  function setInitSegment(
@@ -1,7 +1,4 @@
1
- // eslint-disable-next-line import/no-duplicates
2
- import type { Fragment } from '../loader/fragment';
3
- // eslint-disable-next-line import/no-duplicates
4
- import type { Part } from '../loader/fragment';
1
+ import type { Fragment, MediaFragment, Part } from '../loader/fragment';
5
2
  import type { LevelDetails } from '../loader/level-details';
6
3
  import type {
7
4
  HdcpLevel,
@@ -321,7 +318,7 @@ export interface NonNativeTextTracksData {
321
318
 
322
319
  export interface InitPTSFoundData {
323
320
  id: string;
324
- frag: Fragment;
321
+ frag: MediaFragment;
325
322
  initPTS: number;
326
323
  timescale: number;
327
324
  }