hls.js 1.5.13 → 1.5.14-0.canary.10417

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.
Files changed (103) hide show
  1. package/README.md +4 -3
  2. package/dist/hls-demo.js +41 -38
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +4211 -2666
  5. package/dist/hls.js.d.ts +179 -110
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2841 -1921
  8. package/dist/hls.light.js.map +1 -1
  9. package/dist/hls.light.min.js +1 -1
  10. package/dist/hls.light.min.js.map +1 -1
  11. package/dist/hls.light.mjs +2569 -1639
  12. package/dist/hls.light.mjs.map +1 -1
  13. package/dist/hls.min.js +1 -1
  14. package/dist/hls.min.js.map +1 -1
  15. package/dist/hls.mjs +3572 -2017
  16. package/dist/hls.mjs.map +1 -1
  17. package/dist/hls.worker.js +1 -1
  18. package/dist/hls.worker.js.map +1 -1
  19. package/package.json +38 -38
  20. package/src/config.ts +5 -2
  21. package/src/controller/abr-controller.ts +39 -25
  22. package/src/controller/audio-stream-controller.ts +156 -136
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +27 -10
  25. package/src/controller/base-stream-controller.ts +234 -89
  26. package/src/controller/buffer-controller.ts +250 -97
  27. package/src/controller/buffer-operation-queue.ts +16 -19
  28. package/src/controller/cap-level-controller.ts +3 -2
  29. package/src/controller/cmcd-controller.ts +51 -14
  30. package/src/controller/content-steering-controller.ts +29 -15
  31. package/src/controller/eme-controller.ts +10 -23
  32. package/src/controller/error-controller.ts +28 -22
  33. package/src/controller/fps-controller.ts +8 -3
  34. package/src/controller/fragment-finders.ts +44 -16
  35. package/src/controller/fragment-tracker.ts +58 -25
  36. package/src/controller/gap-controller.ts +43 -16
  37. package/src/controller/id3-track-controller.ts +45 -35
  38. package/src/controller/latency-controller.ts +18 -13
  39. package/src/controller/level-controller.ts +37 -19
  40. package/src/controller/stream-controller.ts +100 -83
  41. package/src/controller/subtitle-stream-controller.ts +35 -47
  42. package/src/controller/subtitle-track-controller.ts +5 -3
  43. package/src/controller/timeline-controller.ts +20 -22
  44. package/src/crypt/aes-crypto.ts +21 -2
  45. package/src/crypt/decrypter-aes-mode.ts +4 -0
  46. package/src/crypt/decrypter.ts +32 -16
  47. package/src/crypt/fast-aes-key.ts +28 -5
  48. package/src/demux/audio/aacdemuxer.ts +2 -2
  49. package/src/demux/audio/ac3-demuxer.ts +4 -3
  50. package/src/demux/audio/adts.ts +9 -4
  51. package/src/demux/audio/base-audio-demuxer.ts +16 -14
  52. package/src/demux/audio/mp3demuxer.ts +4 -3
  53. package/src/demux/audio/mpegaudio.ts +1 -1
  54. package/src/demux/mp4demuxer.ts +7 -7
  55. package/src/demux/sample-aes.ts +2 -0
  56. package/src/demux/transmuxer-interface.ts +8 -16
  57. package/src/demux/transmuxer-worker.ts +4 -4
  58. package/src/demux/transmuxer.ts +16 -3
  59. package/src/demux/tsdemuxer.ts +75 -38
  60. package/src/demux/video/avc-video-parser.ts +210 -121
  61. package/src/demux/video/base-video-parser.ts +135 -2
  62. package/src/demux/video/exp-golomb.ts +0 -208
  63. package/src/demux/video/hevc-video-parser.ts +749 -0
  64. package/src/events.ts +8 -1
  65. package/src/exports-named.ts +1 -1
  66. package/src/hls.ts +84 -47
  67. package/src/loader/date-range.ts +71 -5
  68. package/src/loader/fragment-loader.ts +23 -21
  69. package/src/loader/fragment.ts +8 -4
  70. package/src/loader/key-loader.ts +3 -1
  71. package/src/loader/level-details.ts +6 -6
  72. package/src/loader/level-key.ts +10 -9
  73. package/src/loader/m3u8-parser.ts +138 -144
  74. package/src/loader/playlist-loader.ts +5 -7
  75. package/src/remux/mp4-generator.ts +196 -1
  76. package/src/remux/mp4-remuxer.ts +32 -62
  77. package/src/remux/passthrough-remuxer.ts +1 -1
  78. package/src/task-loop.ts +5 -2
  79. package/src/types/component-api.ts +3 -1
  80. package/src/types/demuxer.ts +3 -0
  81. package/src/types/events.ts +19 -6
  82. package/src/types/fragment-tracker.ts +2 -2
  83. package/src/types/media-playlist.ts +9 -1
  84. package/src/types/remuxer.ts +1 -1
  85. package/src/utils/attr-list.ts +96 -9
  86. package/src/utils/buffer-helper.ts +12 -31
  87. package/src/utils/cea-608-parser.ts +1 -3
  88. package/src/utils/codecs.ts +34 -5
  89. package/src/utils/encryption-methods-util.ts +21 -0
  90. package/src/utils/fetch-loader.ts +1 -1
  91. package/src/utils/hash.ts +10 -0
  92. package/src/utils/hdr.ts +4 -7
  93. package/src/utils/imsc1-ttml-parser.ts +1 -1
  94. package/src/utils/keysystem-util.ts +1 -6
  95. package/src/utils/level-helper.ts +71 -44
  96. package/src/utils/logger.ts +58 -23
  97. package/src/utils/mp4-tools.ts +5 -3
  98. package/src/utils/rendition-helper.ts +100 -74
  99. package/src/utils/utf8-utils.ts +18 -0
  100. package/src/utils/variable-substitution.ts +0 -19
  101. package/src/utils/webvtt-parser.ts +2 -12
  102. package/src/demux/id3.ts +0 -411
  103. package/src/types/general.ts +0 -6
@@ -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
  */
@@ -123,9 +127,9 @@ export class Fragment extends BaseSegment {
123
127
  // The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
124
128
  public endPTS?: number;
125
129
  // The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
126
- public startDTS!: number;
130
+ public startDTS?: number;
127
131
  // The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
128
- public endDTS!: number;
132
+ public endDTS?: number;
129
133
  // The start time of the fragment, as listed in the manifest. Updated after transmux complete.
130
134
  public start: number = 0;
131
135
  // Set by `updateFragPTSDTS` in level-helper
@@ -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,5 +1,5 @@
1
1
  import { ErrorTypes, ErrorDetails } from '../errors';
2
- import {
2
+ import type {
3
3
  LoaderStats,
4
4
  LoaderResponse,
5
5
  LoaderConfiguration,
@@ -194,6 +194,8 @@ export default class KeyLoader implements ComponentAPI {
194
194
  }
195
195
  return this.loadKeyEME(keyInfo, frag);
196
196
  case 'AES-128':
197
+ case 'AES-256':
198
+ case 'AES-256-CTR':
197
199
  return this.loadKeyHTTP(keyInfo, frag);
198
200
  default:
199
201
  return Promise.reject(
@@ -1,7 +1,6 @@
1
- import { Part } from './fragment';
2
- import type { Fragment } from './fragment';
3
- import type { AttrList } from '../utils/attr-list';
1
+ import type { Fragment, MediaFragment, Part } from './fragment';
4
2
  import type { DateRange } from './date-range';
3
+ import type { AttrList } from '../utils/attr-list';
5
4
  import type { VariableMap } from '../types/level';
6
5
 
7
6
  const DEFAULT_TARGET_DURATION = 10;
@@ -15,10 +14,11 @@ export class LevelDetails {
15
14
  public averagetargetduration?: number;
16
15
  public endCC: number = 0;
17
16
  public endSN: number = 0;
18
- public fragments: Fragment[];
19
- public fragmentHint?: Fragment;
17
+ public fragments: MediaFragment[];
18
+ public fragmentHint?: MediaFragment;
20
19
  public partList: Part[] | null = null;
21
20
  public dateRanges: Record<string, DateRange>;
21
+ public dateRangeTagCount: number = 0;
22
22
  public live: boolean = true;
23
23
  public ageHeader: number = 0;
24
24
  public advancedDateTime?: number;
@@ -148,7 +148,7 @@ export class LevelDetails {
148
148
 
149
149
  get lastPartSn(): number {
150
150
  if (this.partList?.length) {
151
- return this.partList[this.partList.length - 1].fragment.sn as number;
151
+ return this.partList[this.partList.length - 1].fragment.sn;
152
152
  }
153
153
  return this.endSN;
154
154
  }
@@ -2,6 +2,7 @@ import {
2
2
  changeEndianness,
3
3
  convertDataUriToArrayBytes,
4
4
  } from '../utils/keysystem-util';
5
+ import { isFullSegmentEncryption } from '../utils/encryption-methods-util';
5
6
  import { KeySystemFormats } from '../utils/mediakeys-helper';
6
7
  import { mp4pssh } from '../utils/mp4-tools';
7
8
  import { logger } from '../utils/logger';
@@ -51,13 +52,14 @@ export class LevelKey implements DecryptData {
51
52
  this.keyFormatVersions = formatversions;
52
53
  this.iv = iv;
53
54
  this.encrypted = method ? method !== 'NONE' : false;
54
- this.isCommonEncryption = this.encrypted && method !== 'AES-128';
55
+ this.isCommonEncryption =
56
+ this.encrypted && !isFullSegmentEncryption(method);
55
57
  }
56
58
 
57
59
  public isSupported(): boolean {
58
60
  // If it's Segment encryption or No encryption, just select that key system
59
61
  if (this.method) {
60
- if (this.method === 'AES-128' || this.method === 'NONE') {
62
+ if (isFullSegmentEncryption(this.method) || this.method === 'NONE') {
61
63
  return true;
62
64
  }
63
65
  if (this.keyFormat === 'identity') {
@@ -88,16 +90,15 @@ export class LevelKey implements DecryptData {
88
90
  return null;
89
91
  }
90
92
 
91
- if (this.method === 'AES-128' && this.uri && !this.iv) {
93
+ if (isFullSegmentEncryption(this.method) && this.uri && !this.iv) {
92
94
  if (typeof sn !== 'number') {
93
95
  // We are fetching decryption data for a initialization segment
94
- // If the segment was encrypted with AES-128
96
+ // If the segment was encrypted with AES-128/256
95
97
  // It must have an IV defined. We cannot substitute the Segment Number in.
96
- if (this.method === 'AES-128' && !this.iv) {
97
- logger.warn(
98
- `missing IV for initialization segment with method="${this.method}" - compliance issue`,
99
- );
100
- }
98
+ logger.warn(
99
+ `missing IV for initialization segment with method="${this.method}" - compliance issue`,
100
+ );
101
+
101
102
  // Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
102
103
  sn = 0;
103
104
  }
@@ -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(
@@ -8,7 +8,6 @@
8
8
 
9
9
  import { Events } from '../events';
10
10
  import { ErrorDetails, ErrorTypes } from '../errors';
11
- import { logger } from '../utils/logger';
12
11
  import M3U8Parser from './m3u8-parser';
13
12
  import type { LevelParsed, VariableMap } from '../types/level';
14
13
  import type {
@@ -221,10 +220,10 @@ class PlaylistLoader implements NetworkComponentAPI {
221
220
  loaderContext.level === context.level
222
221
  ) {
223
222
  // same URL can't overlap
224
- logger.trace('[playlist-loader]: playlist request ongoing');
223
+ this.hls.logger.trace('[playlist-loader]: playlist request ongoing');
225
224
  return;
226
225
  }
227
- logger.log(
226
+ this.hls.logger.log(
228
227
  `[playlist-loader]: aborting previous loader for type: ${context.type}`,
229
228
  );
230
229
  loader.abort();
@@ -408,7 +407,7 @@ class PlaylistLoader implements NetworkComponentAPI {
408
407
  levels[0].audioCodec &&
409
408
  !levels[0].attrs.AUDIO
410
409
  ) {
411
- logger.log(
410
+ this.hls.logger.log(
412
411
  '[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one',
413
412
  );
414
413
  audioTracks.unshift({
@@ -453,7 +452,6 @@ class PlaylistLoader implements NetworkComponentAPI {
453
452
  const { id, level, type } = context;
454
453
 
455
454
  const url = getResponseUrl(response, context);
456
- const levelUrlId = 0;
457
455
  const levelId = Number.isFinite(level as number)
458
456
  ? (level as number)
459
457
  : Number.isFinite(id as number)
@@ -465,7 +463,7 @@ class PlaylistLoader implements NetworkComponentAPI {
465
463
  url,
466
464
  levelId,
467
465
  levelType,
468
- levelUrlId,
466
+ 0,
469
467
  this.variableList,
470
468
  );
471
469
 
@@ -555,7 +553,7 @@ class PlaylistLoader implements NetworkComponentAPI {
555
553
  message += ` id: ${context.id} group-id: "${context.groupId}"`;
556
554
  }
557
555
  const error = new Error(message);
558
- logger.warn(`[playlist-loader]: ${message}`);
556
+ this.hls.logger.warn(`[playlist-loader]: ${message}`);
559
557
  let details = ErrorDetails.UNKNOWN;
560
558
  let fatal = false;
561
559