hls.js 1.5.14-0.canary.10515 → 1.5.14

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 (107) hide show
  1. package/README.md +3 -4
  2. package/dist/hls-demo.js +38 -41
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +2903 -4542
  5. package/dist/hls.js.d.ts +112 -186
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2284 -3295
  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 +1804 -2817
  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 +4652 -6293
  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 +2 -5
  21. package/src/controller/abr-controller.ts +25 -39
  22. package/src/controller/audio-stream-controller.ts +136 -156
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +10 -27
  25. package/src/controller/base-stream-controller.ts +107 -263
  26. package/src/controller/buffer-controller.ts +98 -252
  27. package/src/controller/buffer-operation-queue.ts +19 -16
  28. package/src/controller/cap-level-controller.ts +2 -3
  29. package/src/controller/cmcd-controller.ts +14 -51
  30. package/src/controller/content-steering-controller.ts +15 -29
  31. package/src/controller/eme-controller.ts +23 -10
  32. package/src/controller/error-controller.ts +22 -28
  33. package/src/controller/fps-controller.ts +3 -8
  34. package/src/controller/fragment-finders.ts +16 -44
  35. package/src/controller/fragment-tracker.ts +25 -58
  36. package/src/controller/gap-controller.ts +16 -43
  37. package/src/controller/id3-track-controller.ts +35 -45
  38. package/src/controller/latency-controller.ts +13 -18
  39. package/src/controller/level-controller.ts +19 -37
  40. package/src/controller/stream-controller.ts +83 -100
  41. package/src/controller/subtitle-stream-controller.ts +47 -35
  42. package/src/controller/subtitle-track-controller.ts +3 -5
  43. package/src/controller/timeline-controller.ts +22 -20
  44. package/src/crypt/aes-crypto.ts +2 -21
  45. package/src/crypt/decrypter.ts +16 -32
  46. package/src/crypt/fast-aes-key.ts +5 -28
  47. package/src/demux/audio/aacdemuxer.ts +5 -5
  48. package/src/demux/audio/ac3-demuxer.ts +4 -5
  49. package/src/demux/audio/adts.ts +4 -9
  50. package/src/demux/audio/base-audio-demuxer.ts +14 -16
  51. package/src/demux/audio/mp3demuxer.ts +3 -4
  52. package/src/demux/audio/mpegaudio.ts +1 -1
  53. package/src/demux/id3.ts +411 -0
  54. package/src/demux/inject-worker.ts +4 -38
  55. package/src/demux/mp4demuxer.ts +7 -7
  56. package/src/demux/sample-aes.ts +0 -2
  57. package/src/demux/transmuxer-interface.ts +83 -106
  58. package/src/demux/transmuxer-worker.ts +77 -111
  59. package/src/demux/transmuxer.ts +22 -46
  60. package/src/demux/tsdemuxer.ts +62 -122
  61. package/src/demux/video/avc-video-parser.ts +121 -210
  62. package/src/demux/video/base-video-parser.ts +2 -135
  63. package/src/demux/video/exp-golomb.ts +208 -0
  64. package/src/errors.ts +0 -2
  65. package/src/events.ts +1 -8
  66. package/src/exports-named.ts +1 -1
  67. package/src/hls.ts +48 -97
  68. package/src/loader/date-range.ts +5 -71
  69. package/src/loader/fragment-loader.ts +21 -23
  70. package/src/loader/fragment.ts +4 -8
  71. package/src/loader/key-loader.ts +1 -3
  72. package/src/loader/level-details.ts +6 -6
  73. package/src/loader/level-key.ts +9 -10
  74. package/src/loader/m3u8-parser.ts +144 -138
  75. package/src/loader/playlist-loader.ts +7 -5
  76. package/src/remux/mp4-generator.ts +1 -196
  77. package/src/remux/mp4-remuxer.ts +84 -55
  78. package/src/remux/passthrough-remuxer.ts +8 -23
  79. package/src/task-loop.ts +2 -5
  80. package/src/types/component-api.ts +1 -3
  81. package/src/types/demuxer.ts +0 -3
  82. package/src/types/events.ts +6 -19
  83. package/src/types/fragment-tracker.ts +2 -2
  84. package/src/types/general.ts +6 -0
  85. package/src/types/media-playlist.ts +1 -9
  86. package/src/types/remuxer.ts +1 -1
  87. package/src/utils/attr-list.ts +9 -96
  88. package/src/utils/buffer-helper.ts +31 -12
  89. package/src/utils/cea-608-parser.ts +3 -1
  90. package/src/utils/codecs.ts +5 -34
  91. package/src/utils/discontinuities.ts +47 -21
  92. package/src/utils/fetch-loader.ts +1 -1
  93. package/src/utils/hdr.ts +7 -4
  94. package/src/utils/imsc1-ttml-parser.ts +1 -1
  95. package/src/utils/keysystem-util.ts +6 -1
  96. package/src/utils/level-helper.ts +44 -71
  97. package/src/utils/logger.ts +23 -58
  98. package/src/utils/mp4-tools.ts +3 -5
  99. package/src/utils/rendition-helper.ts +74 -100
  100. package/src/utils/variable-substitution.ts +19 -0
  101. package/src/utils/webvtt-parser.ts +12 -2
  102. package/src/crypt/decrypter-aes-mode.ts +0 -4
  103. package/src/demux/video/hevc-video-parser.ts +0 -749
  104. package/src/utils/encryption-methods-util.ts +0 -21
  105. package/src/utils/hash.ts +0 -10
  106. package/src/utils/utf8-utils.ts +0 -18
  107. package/src/version.ts +0 -1
@@ -3,17 +3,16 @@
3
3
  */
4
4
 
5
5
  import { logger } from './logger';
6
- import { DateRange } from '../loader/date-range';
7
- import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
8
- import type { Fragment, MediaFragment, Part } from '../loader/fragment';
9
- import type { LevelDetails } from '../loader/level-details';
6
+ import { Fragment, Part } from '../loader/fragment';
7
+ import { LevelDetails } from '../loader/level-details';
10
8
  import type { Level } from '../types/level';
9
+ import { DateRange } from '../loader/date-range';
11
10
 
12
11
  type FragmentIntersection = (oldFrag: Fragment, newFrag: Fragment) => void;
13
12
  type PartIntersection = (oldPart: Part, newPart: Part) => void;
14
13
 
15
14
  export function updatePTS(
16
- fragments: MediaFragment[],
15
+ fragments: Fragment[],
17
16
  fromIdx: number,
18
17
  toIdx: number,
19
18
  ): void {
@@ -22,7 +21,7 @@ export function updatePTS(
22
21
  updateFromToPTS(fragFrom, fragTo);
23
22
  }
24
23
 
25
- function updateFromToPTS(fragFrom: MediaFragment, fragTo: MediaFragment) {
24
+ function updateFromToPTS(fragFrom: Fragment, fragTo: Fragment) {
26
25
  const fragToPTS = fragTo.startPTS as number;
27
26
  // if we know startPTS[toIdx]
28
27
  if (Number.isFinite(fragToPTS)) {
@@ -56,7 +55,7 @@ function updateFromToPTS(fragFrom: MediaFragment, fragTo: MediaFragment) {
56
55
 
57
56
  export function updateFragPTSDTS(
58
57
  details: LevelDetails | undefined,
59
- frag: MediaFragment,
58
+ frag: Fragment,
60
59
  startPTS: number,
61
60
  endPTS: number,
62
61
  startDTS: number,
@@ -83,11 +82,11 @@ export function updateFragPTSDTS(
83
82
 
84
83
  maxStartPTS = Math.max(startPTS, fragStartPts);
85
84
  startPTS = Math.min(startPTS, fragStartPts);
86
- startDTS = Math.min(startDTS, frag.startDTS as number);
85
+ startDTS = Math.min(startDTS, frag.startDTS);
87
86
 
88
87
  minEndPTS = Math.min(endPTS, fragEndPts);
89
88
  endPTS = Math.max(endPTS, fragEndPts);
90
- endDTS = Math.max(endDTS, frag.endDTS as number);
89
+ endDTS = Math.max(endDTS, frag.endDTS);
91
90
  }
92
91
 
93
92
  const drift = startPTS - frag.start;
@@ -102,7 +101,7 @@ export function updateFragPTSDTS(
102
101
  frag.minEndPTS = minEndPTS;
103
102
  frag.endDTS = endDTS;
104
103
 
105
- const sn = frag.sn;
104
+ const sn = frag.sn as number; // 'initSegment'
106
105
  // exit if sn out of range
107
106
  if (!details || sn < details.startSN || sn > details.endSN) {
108
107
  return 0;
@@ -197,10 +196,10 @@ export function mergeDetails(
197
196
  },
198
197
  );
199
198
 
200
- const fragmentsToCheck = newDetails.fragmentHint
201
- ? newDetails.fragments.concat(newDetails.fragmentHint)
202
- : newDetails.fragments;
203
199
  if (currentInitSegment) {
200
+ const fragmentsToCheck = newDetails.fragmentHint
201
+ ? newDetails.fragments.concat(newDetails.fragmentHint)
202
+ : newDetails.fragments;
204
203
  fragmentsToCheck.forEach((frag) => {
205
204
  if (
206
205
  frag &&
@@ -221,30 +220,14 @@ export function mergeDetails(
221
220
  for (let i = newDetails.skippedSegments; i--; ) {
222
221
  newDetails.fragments.shift();
223
222
  }
224
- newDetails.startSN = newDetails.fragments[0].sn;
223
+ newDetails.startSN = newDetails.fragments[0].sn as number;
225
224
  newDetails.startCC = newDetails.fragments[0].cc;
226
- } else {
227
- if (newDetails.canSkipDateRanges) {
228
- newDetails.dateRanges = mergeDateRanges(
229
- oldDetails.dateRanges,
230
- newDetails,
231
- );
232
- }
233
- const programDateTimes = oldDetails.fragments.filter(
234
- (frag) => frag.rawProgramDateTime,
225
+ } else if (newDetails.canSkipDateRanges) {
226
+ newDetails.dateRanges = mergeDateRanges(
227
+ oldDetails.dateRanges,
228
+ newDetails.dateRanges,
229
+ newDetails.recentlyRemovedDateranges,
235
230
  );
236
- if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) {
237
- for (let i = 1; i < fragmentsToCheck.length; i++) {
238
- if (fragmentsToCheck[i].programDateTime === null) {
239
- assignProgramDateTime(
240
- fragmentsToCheck[i],
241
- fragmentsToCheck[i - 1],
242
- programDateTimes,
243
- );
244
- }
245
- }
246
- }
247
- mapDateRanges(programDateTimes, newDetails);
248
231
  }
249
232
  }
250
233
 
@@ -310,38 +293,27 @@ export function mergeDetails(
310
293
 
311
294
  function mergeDateRanges(
312
295
  oldDateRanges: Record<string, DateRange>,
313
- newDetails: LevelDetails,
296
+ deltaDateRanges: Record<string, DateRange>,
297
+ recentlyRemovedDateranges: string[] | undefined,
314
298
  ): Record<string, DateRange> {
315
- const { dateRanges: deltaDateRanges, recentlyRemovedDateranges } = newDetails;
316
299
  const dateRanges = Object.assign({}, oldDateRanges);
317
300
  if (recentlyRemovedDateranges) {
318
301
  recentlyRemovedDateranges.forEach((id) => {
319
302
  delete dateRanges[id];
320
303
  });
321
304
  }
322
- const mergeIds = Object.keys(dateRanges);
323
- const mergeCount = mergeIds.length;
324
- if (mergeCount) {
325
- Object.keys(deltaDateRanges).forEach((id) => {
326
- const mergedDateRange = dateRanges[id];
327
- const dateRange = new DateRange(
328
- deltaDateRanges[id].attr,
329
- mergedDateRange,
305
+ Object.keys(deltaDateRanges).forEach((id) => {
306
+ const dateRange = new DateRange(deltaDateRanges[id].attr, dateRanges[id]);
307
+ if (dateRange.isValid) {
308
+ dateRanges[id] = dateRange;
309
+ } else {
310
+ logger.warn(
311
+ `Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(
312
+ deltaDateRanges[id].attr,
313
+ )}"`,
330
314
  );
331
- if (dateRange.isValid) {
332
- dateRanges[id] = dateRange;
333
- if (!mergedDateRange) {
334
- dateRange.tagOrder += mergeCount;
335
- }
336
- } else {
337
- logger.warn(
338
- `Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(
339
- deltaDateRanges[id].attr,
340
- )}"`,
341
- );
342
- }
343
- });
344
- }
315
+ }
316
+ });
345
317
  return dateRanges;
346
318
  }
347
319
 
@@ -394,7 +366,7 @@ export function mapFragmentIntersection(
394
366
  for (let i = start; i <= end; i++) {
395
367
  const oldFrag = oldFrags[delta + i];
396
368
  let newFrag = newFrags[i];
397
- if (skippedSegments && !newFrag && oldFrag) {
369
+ if (skippedSegments && !newFrag && i < skippedSegments) {
398
370
  // Fill in skipped segments in delta playlist
399
371
  newFrag = newDetails.fragments[i] = oldFrag;
400
372
  }
@@ -461,37 +433,38 @@ export function computeReloadInterval(
461
433
  }
462
434
 
463
435
  export function getFragmentWithSN(
464
- details: LevelDetails | undefined,
436
+ level: Level,
465
437
  sn: number,
466
438
  fragCurrent: Fragment | null,
467
- ): MediaFragment | null {
468
- if (!details) {
439
+ ): Fragment | null {
440
+ if (!level?.details) {
469
441
  return null;
470
442
  }
471
- let fragment: MediaFragment | undefined =
472
- details.fragments[sn - details.startSN];
443
+ const levelDetails = level.details;
444
+ let fragment: Fragment | undefined =
445
+ levelDetails.fragments[sn - levelDetails.startSN];
473
446
  if (fragment) {
474
447
  return fragment;
475
448
  }
476
- fragment = details.fragmentHint;
449
+ fragment = levelDetails.fragmentHint;
477
450
  if (fragment && fragment.sn === sn) {
478
451
  return fragment;
479
452
  }
480
- if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) {
481
- return fragCurrent as MediaFragment;
453
+ if (sn < levelDetails.startSN && fragCurrent && fragCurrent.sn === sn) {
454
+ return fragCurrent;
482
455
  }
483
456
  return null;
484
457
  }
485
458
 
486
459
  export function getPartWith(
487
- details: LevelDetails | undefined,
460
+ level: Level,
488
461
  sn: number,
489
462
  partIndex: number,
490
463
  ): Part | null {
491
- if (!details) {
464
+ if (!level?.details) {
492
465
  return null;
493
466
  }
494
- return findPart(details.partList, sn, partIndex);
467
+ return findPart(level.details?.partList, sn, partIndex);
495
468
  }
496
469
 
497
470
  export function findPart(
@@ -11,25 +11,6 @@ export interface ILogger {
11
11
  error: ILogFunction;
12
12
  }
13
13
 
14
- export class Logger implements ILogger {
15
- trace: ILogFunction;
16
- debug: ILogFunction;
17
- log: ILogFunction;
18
- warn: ILogFunction;
19
- info: ILogFunction;
20
- error: ILogFunction;
21
-
22
- constructor(label: string, logger: ILogger) {
23
- const lb = `[${label}]:`;
24
- this.trace = noop;
25
- this.debug = logger.debug.bind(null, lb);
26
- this.log = logger.log.bind(null, lb);
27
- this.warn = logger.warn.bind(null, lb);
28
- this.info = logger.info.bind(null, lb);
29
- this.error = logger.error.bind(null, lb);
30
- }
31
- }
32
-
33
14
  const noop: ILogFunction = function () {};
34
15
 
35
16
  const fakeLogger: ILogger = {
@@ -41,9 +22,7 @@ const fakeLogger: ILogger = {
41
22
  error: noop,
42
23
  };
43
24
 
44
- function createLogger() {
45
- return Object.assign({}, fakeLogger);
46
- }
25
+ let exportedLogger: ILogger = fakeLogger;
47
26
 
48
27
  // let lastCallTime;
49
28
  // function formatMsgWithTimeInfo(type, msg) {
@@ -54,37 +33,33 @@ function createLogger() {
54
33
  // return msg;
55
34
  // }
56
35
 
57
- function consolePrintFn(type: string, id: string | undefined): ILogFunction {
36
+ function consolePrintFn(type: string): ILogFunction {
58
37
  const func: ILogFunction = self.console[type];
59
- return func
60
- ? func.bind(self.console, `${id ? '[' + id + '] ' : ''}[${type}] >`)
61
- : noop;
38
+ if (func) {
39
+ return func.bind(self.console, `[${type}] >`);
40
+ }
41
+ return noop;
62
42
  }
63
43
 
64
- function getLoggerFn(
65
- key: string,
66
- debugConfig: boolean | Partial<ILogger>,
67
- id?: string,
68
- ): ILogFunction {
69
- return debugConfig[key]
70
- ? debugConfig[key].bind(debugConfig)
71
- : consolePrintFn(key, id);
44
+ function exportLoggerFunctions(
45
+ debugConfig: boolean | ILogger,
46
+ ...functions: string[]
47
+ ): void {
48
+ functions.forEach(function (type) {
49
+ exportedLogger[type] = debugConfig[type]
50
+ ? debugConfig[type].bind(debugConfig)
51
+ : consolePrintFn(type);
52
+ });
72
53
  }
73
54
 
74
- const exportedLogger: ILogger = createLogger();
75
-
76
- export function enableLogs(
77
- debugConfig: boolean | ILogger,
78
- context: string,
79
- id?: string | undefined,
80
- ): ILogger {
55
+ export function enableLogs(debugConfig: boolean | ILogger, id: string): void {
81
56
  // check that console is available
82
- const newLogger = createLogger();
83
57
  if (
84
58
  (typeof console === 'object' && debugConfig === true) ||
85
59
  typeof debugConfig === 'object'
86
60
  ) {
87
- const keys: (keyof ILogger)[] = [
61
+ exportLoggerFunctions(
62
+ debugConfig,
88
63
  // Remove out from list here to hard-disable a log-level
89
64
  // 'trace',
90
65
  'debug',
@@ -92,29 +67,19 @@ export function enableLogs(
92
67
  'info',
93
68
  'warn',
94
69
  'error',
95
- ];
96
- keys.forEach((key) => {
97
- newLogger[key] = getLoggerFn(key, debugConfig, id);
98
- });
70
+ );
99
71
  // Some browsers don't allow to use bind on console object anyway
100
72
  // fallback to default if needed
101
73
  try {
102
- newLogger.log(
103
- `Debug logs enabled for "${context}" in hls.js version ${__VERSION__}`,
74
+ exportedLogger.log(
75
+ `Debug logs enabled for "${id}" in hls.js version ${__VERSION__}`,
104
76
  );
105
77
  } catch (e) {
106
- /* log fn threw an exception. All logger methods are no-ops. */
107
- return createLogger();
78
+ exportedLogger = fakeLogger;
108
79
  }
109
- // global exported logger uses the same functions as new logger without `id`
110
- keys.forEach((key) => {
111
- exportedLogger[key] = getLoggerFn(key, debugConfig);
112
- });
113
80
  } else {
114
- // Reset global exported logger
115
- Object.assign(exportedLogger, newLogger);
81
+ exportedLogger = fakeLogger;
116
82
  }
117
- return newLogger;
118
83
  }
119
84
 
120
85
  export const logger: ILogger = exportedLogger;
@@ -1,6 +1,6 @@
1
1
  import { ElementaryStreamTypes } from '../loader/fragment';
2
2
  import { sliceUint8 } from './typed-array';
3
- import { utf8ArrayToStr } from '@svta/common-media-library/utils/utf8ArrayToStr';
3
+ import { utf8ArrayToStr } from '../demux/id3';
4
4
  import { logger } from '../utils/logger';
5
5
  import Hex from './hex';
6
6
  import type { PassthroughTrack, UserdataSample } from '../types/demuxer';
@@ -327,7 +327,7 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
327
327
  case 'mp4a': {
328
328
  const codecBox = findBox(sampleEntries, [fourCC])[0];
329
329
  const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0];
330
- if (esdsBox && esdsBox.length > 7) {
330
+ if (esdsBox && esdsBox.length > 12) {
331
331
  let i = 4;
332
332
  // ES Descriptor tag
333
333
  if (esdsBox[i++] !== 0x03) {
@@ -478,9 +478,7 @@ function parseStsd(stsd: Uint8Array): { codec: string; encrypted: boolean } {
478
478
 
479
479
  function skipBERInteger(bytes: Uint8Array, i: number): number {
480
480
  const limit = i + 5;
481
- while (bytes[i++] & 0x80 && i < limit) {
482
- /* do nothing */
483
- }
481
+ while (bytes[i++] & 0x80 && i < limit) {}
484
482
  return i;
485
483
  }
486
484
 
@@ -13,7 +13,6 @@ export type CodecSetTier = {
13
13
  minBitrate: number;
14
14
  minHeight: number;
15
15
  minFramerate: number;
16
- minIndex: number;
17
16
  maxScore: number;
18
17
  videoRanges: Record<string, number>;
19
18
  channels: Record<string, number>;
@@ -33,7 +32,6 @@ type StartParameters = {
33
32
  preferHDR: boolean;
34
33
  minFramerate: number;
35
34
  minBitrate: number;
36
- minIndex: number;
37
35
  };
38
36
 
39
37
  export function getStartCodecTier(
@@ -46,15 +44,13 @@ export function getStartCodecTier(
46
44
  const codecSets = Object.keys(codecTiers);
47
45
  const channelsPreference = audioPreference?.channels;
48
46
  const audioCodecPreference = audioPreference?.audioCodec;
49
- const videoCodecPreference = videoPreference?.videoCodec;
50
47
  const preferStereo = channelsPreference && parseInt(channelsPreference) === 2;
51
48
  // Use first level set to determine stereo, and minimum resolution and framerate
52
- let hasStereo = false;
49
+ let hasStereo = true;
53
50
  let hasCurrentVideoRange = false;
54
51
  let minHeight = Infinity;
55
52
  let minFramerate = Infinity;
56
53
  let minBitrate = Infinity;
57
- let minIndex = Infinity;
58
54
  let selectedScore = 0;
59
55
  let videoRanges: Array<VideoRange> = [];
60
56
 
@@ -65,7 +61,7 @@ export function getStartCodecTier(
65
61
 
66
62
  for (let i = codecSets.length; i--; ) {
67
63
  const tier = codecTiers[codecSets[i]];
68
- hasStereo ||= tier.channels[2] > 0;
64
+ hasStereo = tier.channels[2] > 0;
69
65
  minHeight = Math.min(minHeight, tier.minHeight);
70
66
  minFramerate = Math.min(minFramerate, tier.minFramerate);
71
67
  minBitrate = Math.min(minBitrate, tier.minBitrate);
@@ -74,6 +70,7 @@ export function getStartCodecTier(
74
70
  );
75
71
  if (matchingVideoRanges.length > 0) {
76
72
  hasCurrentVideoRange = true;
73
+ videoRanges = matchingVideoRanges;
77
74
  }
78
75
  }
79
76
  minHeight = Number.isFinite(minHeight) ? minHeight : 0;
@@ -85,8 +82,8 @@ export function getStartCodecTier(
85
82
  // If there are no variants with matching preference, set currentVideoRange to undefined
86
83
  if (!hasCurrentVideoRange) {
87
84
  currentVideoRange = undefined;
85
+ videoRanges = [];
88
86
  }
89
- const hasMultipleSets = codecSets.length > 1;
90
87
  const codecSet = codecSets.reduce(
91
88
  (selected: string | undefined, candidate: string) => {
92
89
  // Remove candiates which do not meet bitrate, default audio, stereo or channels preference, 1080p or lower, 30fps or lower, or SDR/HDR selection if present
@@ -94,99 +91,80 @@ export function getStartCodecTier(
94
91
  if (candidate === selected) {
95
92
  return selected;
96
93
  }
97
- videoRanges = hasCurrentVideoRange
98
- ? allowedVideoRanges.filter(
99
- (range) => candidateTier.videoRanges[range] > 0,
100
- )
101
- : [];
102
- if (hasMultipleSets) {
103
- if (candidateTier.minBitrate > currentBw) {
104
- logStartCodecCandidateIgnored(
105
- candidate,
106
- `min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`,
107
- );
108
- return selected;
109
- }
110
- if (!candidateTier.hasDefaultAudio) {
111
- logStartCodecCandidateIgnored(
112
- candidate,
113
- `no renditions with default or auto-select sound found`,
114
- );
115
- return selected;
116
- }
117
- if (
118
- audioCodecPreference &&
119
- candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0
120
- ) {
121
- logStartCodecCandidateIgnored(
122
- candidate,
123
- `audio codec preference "${audioCodecPreference}" not found`,
124
- );
125
- return selected;
126
- }
127
- if (channelsPreference && !preferStereo) {
128
- if (!candidateTier.channels[channelsPreference]) {
129
- logStartCodecCandidateIgnored(
130
- candidate,
131
- `no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys(
132
- candidateTier.channels,
133
- )})`,
134
- );
135
- return selected;
136
- }
137
- } else if (
138
- (!audioCodecPreference || preferStereo) &&
139
- hasStereo &&
140
- candidateTier.channels['2'] === 0
141
- ) {
142
- logStartCodecCandidateIgnored(
143
- candidate,
144
- `no renditions with stereo sound found`,
145
- );
146
- return selected;
147
- }
148
- if (candidateTier.minHeight > maxHeight) {
149
- logStartCodecCandidateIgnored(
150
- candidate,
151
- `min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`,
152
- );
153
- return selected;
154
- }
155
- if (candidateTier.minFramerate > maxFramerate) {
156
- logStartCodecCandidateIgnored(
157
- candidate,
158
- `min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`,
159
- );
160
- return selected;
161
- }
162
- if (
163
- !videoRanges.some((range) => candidateTier.videoRanges[range] > 0)
164
- ) {
165
- logStartCodecCandidateIgnored(
166
- candidate,
167
- `no variants with VIDEO-RANGE of ${JSON.stringify(
168
- videoRanges,
169
- )} found`,
170
- );
171
- return selected;
172
- }
173
- if (
174
- videoCodecPreference &&
175
- candidate.indexOf(videoCodecPreference.substring(0, 4)) % 5 !== 0
176
- ) {
177
- logStartCodecCandidateIgnored(
178
- candidate,
179
- `video codec preference "${videoCodecPreference}" not found`,
180
- );
181
- return selected;
182
- }
183
- if (candidateTier.maxScore < selectedScore) {
94
+ if (candidateTier.minBitrate > currentBw) {
95
+ logStartCodecCandidateIgnored(
96
+ candidate,
97
+ `min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`,
98
+ );
99
+ return selected;
100
+ }
101
+ if (!candidateTier.hasDefaultAudio) {
102
+ logStartCodecCandidateIgnored(
103
+ candidate,
104
+ `no renditions with default or auto-select sound found`,
105
+ );
106
+ return selected;
107
+ }
108
+ if (
109
+ audioCodecPreference &&
110
+ candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0
111
+ ) {
112
+ logStartCodecCandidateIgnored(
113
+ candidate,
114
+ `audio codec preference "${audioCodecPreference}" not found`,
115
+ );
116
+ return selected;
117
+ }
118
+ if (channelsPreference && !preferStereo) {
119
+ if (!candidateTier.channels[channelsPreference]) {
184
120
  logStartCodecCandidateIgnored(
185
121
  candidate,
186
- `max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`,
122
+ `no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys(
123
+ candidateTier.channels,
124
+ )})`,
187
125
  );
188
126
  return selected;
189
127
  }
128
+ } else if (
129
+ (!audioCodecPreference || preferStereo) &&
130
+ hasStereo &&
131
+ candidateTier.channels['2'] === 0
132
+ ) {
133
+ logStartCodecCandidateIgnored(
134
+ candidate,
135
+ `no renditions with stereo sound found`,
136
+ );
137
+ return selected;
138
+ }
139
+ if (candidateTier.minHeight > maxHeight) {
140
+ logStartCodecCandidateIgnored(
141
+ candidate,
142
+ `min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`,
143
+ );
144
+ return selected;
145
+ }
146
+ if (candidateTier.minFramerate > maxFramerate) {
147
+ logStartCodecCandidateIgnored(
148
+ candidate,
149
+ `min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`,
150
+ );
151
+ return selected;
152
+ }
153
+ if (!videoRanges.some((range) => candidateTier.videoRanges[range] > 0)) {
154
+ logStartCodecCandidateIgnored(
155
+ candidate,
156
+ `no variants with VIDEO-RANGE of ${JSON.stringify(
157
+ videoRanges,
158
+ )} found`,
159
+ );
160
+ return selected;
161
+ }
162
+ if (candidateTier.maxScore < selectedScore) {
163
+ logStartCodecCandidateIgnored(
164
+ candidate,
165
+ `max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`,
166
+ );
167
+ return selected;
190
168
  }
191
169
  // Remove candiates with less preferred codecs or more errors
192
170
  if (
@@ -197,7 +175,6 @@ export function getStartCodecTier(
197
175
  ) {
198
176
  return selected;
199
177
  }
200
- minIndex = candidateTier.minIndex;
201
178
  selectedScore = candidateTier.maxScore;
202
179
  return candidate;
203
180
  },
@@ -209,7 +186,6 @@ export function getStartCodecTier(
209
186
  preferHDR,
210
187
  minFramerate,
211
188
  minBitrate,
212
- minIndex,
213
189
  };
214
190
  }
215
191
 
@@ -267,7 +243,7 @@ export function getCodecTiers(
267
243
  ): Record<string, CodecSetTier> {
268
244
  return levels
269
245
  .slice(minAutoLevel, maxAutoLevel + 1)
270
- .reduce((tiers: Record<string, CodecSetTier>, level, index) => {
246
+ .reduce((tiers: Record<string, CodecSetTier>, level) => {
271
247
  if (!level.codecSet) {
272
248
  return tiers;
273
249
  }
@@ -278,7 +254,6 @@ export function getCodecTiers(
278
254
  minBitrate: Infinity,
279
255
  minHeight: Infinity,
280
256
  minFramerate: Infinity,
281
- minIndex: index,
282
257
  maxScore: 0,
283
258
  videoRanges: { SDR: 0 },
284
259
  channels: { '2': 0 },
@@ -290,7 +265,6 @@ export function getCodecTiers(
290
265
  const lesserWidthOrHeight = Math.min(level.height, level.width);
291
266
  tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight);
292
267
  tier.minFramerate = Math.min(tier.minFramerate, level.frameRate);
293
- tier.minIndex = Math.min(tier.minIndex, index);
294
268
  tier.maxScore = Math.max(tier.maxScore, level.score);
295
269
  tier.fragmentError += level.fragmentError;
296
270
  tier.videoRanges[level.videoRange] =
@@ -9,6 +9,25 @@ export function hasVariableReferences(str: string): boolean {
9
9
  return VARIABLE_REPLACEMENT_REGEX.test(str);
10
10
  }
11
11
 
12
+ export function substituteVariablesInAttributes(
13
+ parsed: Pick<
14
+ ParsedMultivariantPlaylist | LevelDetails,
15
+ 'variableList' | 'hasVariableRefs' | 'playlistParsingError'
16
+ >,
17
+ attr: AttrList,
18
+ attributeNames: string[],
19
+ ) {
20
+ if (parsed.variableList !== null || parsed.hasVariableRefs) {
21
+ for (let i = attributeNames.length; i--; ) {
22
+ const name = attributeNames[i];
23
+ const value = attr[name];
24
+ if (value) {
25
+ attr[name] = substituteVariables(parsed, value);
26
+ }
27
+ }
28
+ }
29
+ }
30
+
12
31
  export function substituteVariables(
13
32
  parsed: Pick<
14
33
  ParsedMultivariantPlaylist | LevelDetails,