hls.js 1.5.7 → 1.5.8-0.canary.10046

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 (72) hide show
  1. package/README.md +2 -1
  2. package/dist/hls-demo.js +10 -0
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +2314 -1298
  5. package/dist/hls.js.d.ts +97 -84
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +1486 -1075
  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 +1195 -789
  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 +1979 -982
  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 +22 -22
  20. package/src/config.ts +3 -2
  21. package/src/controller/abr-controller.ts +24 -20
  22. package/src/controller/audio-stream-controller.ts +68 -74
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +20 -8
  25. package/src/controller/base-stream-controller.ts +157 -36
  26. package/src/controller/buffer-controller.ts +203 -67
  27. package/src/controller/buffer-operation-queue.ts +16 -19
  28. package/src/controller/cap-level-controller.ts +2 -2
  29. package/src/controller/cmcd-controller.ts +27 -6
  30. package/src/controller/content-steering-controller.ts +8 -6
  31. package/src/controller/eme-controller.ts +9 -22
  32. package/src/controller/error-controller.ts +6 -8
  33. package/src/controller/fps-controller.ts +2 -3
  34. package/src/controller/fragment-tracker.ts +15 -11
  35. package/src/controller/gap-controller.ts +43 -16
  36. package/src/controller/latency-controller.ts +9 -11
  37. package/src/controller/level-controller.ts +12 -18
  38. package/src/controller/stream-controller.ts +36 -31
  39. package/src/controller/subtitle-stream-controller.ts +28 -40
  40. package/src/controller/subtitle-track-controller.ts +5 -3
  41. package/src/controller/timeline-controller.ts +23 -30
  42. package/src/crypt/aes-crypto.ts +21 -2
  43. package/src/crypt/decrypter-aes-mode.ts +4 -0
  44. package/src/crypt/decrypter.ts +32 -18
  45. package/src/crypt/fast-aes-key.ts +24 -5
  46. package/src/demux/audio/adts.ts +9 -4
  47. package/src/demux/sample-aes.ts +2 -0
  48. package/src/demux/transmuxer-interface.ts +4 -12
  49. package/src/demux/transmuxer-worker.ts +4 -4
  50. package/src/demux/transmuxer.ts +16 -3
  51. package/src/demux/tsdemuxer.ts +71 -37
  52. package/src/demux/video/avc-video-parser.ts +208 -119
  53. package/src/demux/video/base-video-parser.ts +134 -2
  54. package/src/demux/video/exp-golomb.ts +0 -208
  55. package/src/demux/video/hevc-video-parser.ts +746 -0
  56. package/src/events.ts +7 -0
  57. package/src/hls.ts +49 -37
  58. package/src/loader/fragment-loader.ts +9 -2
  59. package/src/loader/key-loader.ts +2 -0
  60. package/src/loader/level-key.ts +10 -9
  61. package/src/loader/playlist-loader.ts +4 -5
  62. package/src/remux/mp4-generator.ts +196 -1
  63. package/src/remux/mp4-remuxer.ts +23 -7
  64. package/src/task-loop.ts +5 -2
  65. package/src/types/component-api.ts +2 -0
  66. package/src/types/demuxer.ts +3 -0
  67. package/src/types/events.ts +4 -0
  68. package/src/utils/buffer-helper.ts +12 -31
  69. package/src/utils/codecs.ts +34 -5
  70. package/src/utils/encryption-methods-util.ts +21 -0
  71. package/src/utils/logger.ts +54 -24
  72. package/src/utils/mp4-tools.ts +4 -2
@@ -256,6 +256,7 @@ let Events = /*#__PURE__*/function (Events) {
256
256
  Events["MEDIA_ATTACHED"] = "hlsMediaAttached";
257
257
  Events["MEDIA_DETACHING"] = "hlsMediaDetaching";
258
258
  Events["MEDIA_DETACHED"] = "hlsMediaDetached";
259
+ Events["MEDIA_ENDED"] = "hlsMediaEnded";
259
260
  Events["BUFFER_RESET"] = "hlsBufferReset";
260
261
  Events["BUFFER_CODECS"] = "hlsBufferCodecs";
261
262
  Events["BUFFER_CREATED"] = "hlsBufferCreated";
@@ -369,58 +370,6 @@ let ErrorDetails = /*#__PURE__*/function (ErrorDetails) {
369
370
  return ErrorDetails;
370
371
  }({});
371
372
 
372
- const noop = function noop() {};
373
- const fakeLogger = {
374
- trace: noop,
375
- debug: noop,
376
- log: noop,
377
- warn: noop,
378
- info: noop,
379
- error: noop
380
- };
381
- let exportedLogger = fakeLogger;
382
-
383
- // let lastCallTime;
384
- // function formatMsgWithTimeInfo(type, msg) {
385
- // const now = Date.now();
386
- // const diff = lastCallTime ? '+' + (now - lastCallTime) : '0';
387
- // lastCallTime = now;
388
- // msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )';
389
- // return msg;
390
- // }
391
-
392
- function consolePrintFn(type) {
393
- const func = self.console[type];
394
- if (func) {
395
- return func.bind(self.console, `[${type}] >`);
396
- }
397
- return noop;
398
- }
399
- function exportLoggerFunctions(debugConfig, ...functions) {
400
- functions.forEach(function (type) {
401
- exportedLogger[type] = debugConfig[type] ? debugConfig[type].bind(debugConfig) : consolePrintFn(type);
402
- });
403
- }
404
- function enableLogs(debugConfig, id) {
405
- // check that console is available
406
- if (typeof console === 'object' && debugConfig === true || typeof debugConfig === 'object') {
407
- exportLoggerFunctions(debugConfig,
408
- // Remove out from list here to hard-disable a log-level
409
- // 'trace',
410
- 'debug', 'log', 'info', 'warn', 'error');
411
- // Some browsers don't allow to use bind on console object anyway
412
- // fallback to default if needed
413
- try {
414
- exportedLogger.log(`Debug logs enabled for "${id}" in hls.js version ${"1.5.7"}`);
415
- } catch (e) {
416
- exportedLogger = fakeLogger;
417
- }
418
- } else {
419
- exportedLogger = fakeLogger;
420
- }
421
- }
422
- const logger = exportedLogger;
423
-
424
373
  const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/;
425
374
  const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g;
426
375
 
@@ -502,6 +451,79 @@ class AttrList {
502
451
  }
503
452
  }
504
453
 
454
+ class Logger {
455
+ constructor(label, logger) {
456
+ this.trace = void 0;
457
+ this.debug = void 0;
458
+ this.log = void 0;
459
+ this.warn = void 0;
460
+ this.info = void 0;
461
+ this.error = void 0;
462
+ const lb = `[${label}]:`;
463
+ this.trace = noop;
464
+ this.debug = logger.debug.bind(null, lb);
465
+ this.log = logger.log.bind(null, lb);
466
+ this.warn = logger.warn.bind(null, lb);
467
+ this.info = logger.info.bind(null, lb);
468
+ this.error = logger.error.bind(null, lb);
469
+ }
470
+ }
471
+ const noop = function noop() {};
472
+ const fakeLogger = {
473
+ trace: noop,
474
+ debug: noop,
475
+ log: noop,
476
+ warn: noop,
477
+ info: noop,
478
+ error: noop
479
+ };
480
+ function createLogger() {
481
+ return _extends({}, fakeLogger);
482
+ }
483
+
484
+ // let lastCallTime;
485
+ // function formatMsgWithTimeInfo(type, msg) {
486
+ // const now = Date.now();
487
+ // const diff = lastCallTime ? '+' + (now - lastCallTime) : '0';
488
+ // lastCallTime = now;
489
+ // msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )';
490
+ // return msg;
491
+ // }
492
+
493
+ function consolePrintFn(type, id) {
494
+ const func = self.console[type];
495
+ return func ? func.bind(self.console, `${id ? '[' + id + '] ' : ''}[${type}] >`) : noop;
496
+ }
497
+ function getLoggerFn(key, debugConfig, id) {
498
+ return debugConfig[key] ? debugConfig[key].bind(debugConfig) : consolePrintFn(key, id);
499
+ }
500
+ const exportedLogger = createLogger();
501
+ function enableLogs(debugConfig, context, id) {
502
+ // check that console is available
503
+ const newLogger = createLogger();
504
+ if (typeof console === 'object' && debugConfig === true || typeof debugConfig === 'object') {
505
+ const keys = [
506
+ // Remove out from list here to hard-disable a log-level
507
+ // 'trace',
508
+ 'debug', 'log', 'info', 'warn', 'error'];
509
+ keys.forEach(key => {
510
+ newLogger[key] = getLoggerFn(key, debugConfig, id);
511
+ });
512
+ // Some browsers don't allow to use bind on console object anyway
513
+ // fallback to default if needed
514
+ try {
515
+ newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.5.8-0.canary.10046"}`);
516
+ } catch (e) {
517
+ /* log fn threw an exception. All logger methods are no-ops. */
518
+ return createLogger();
519
+ }
520
+ }
521
+ // global exported logger uses the log methods from last call to `enableLogs`
522
+ _extends(exportedLogger, newLogger);
523
+ return newLogger;
524
+ }
525
+ const logger = exportedLogger;
526
+
505
527
  // Avoid exporting const enum so that these values can be inlined
506
528
 
507
529
  function isDateRangeCueAttribute(attrName) {
@@ -991,10 +1013,30 @@ class LevelDetails {
991
1013
  }
992
1014
  }
993
1015
 
1016
+ var DecrypterAesMode = {
1017
+ cbc: 0,
1018
+ ctr: 1
1019
+ };
1020
+
1021
+ function isFullSegmentEncryption(method) {
1022
+ return method === 'AES-128' || method === 'AES-256' || method === 'AES-256-CTR';
1023
+ }
1024
+ function getAesModeFromFullSegmentMethod(method) {
1025
+ switch (method) {
1026
+ case 'AES-128':
1027
+ case 'AES-256':
1028
+ return DecrypterAesMode.cbc;
1029
+ case 'AES-256-CTR':
1030
+ return DecrypterAesMode.ctr;
1031
+ default:
1032
+ throw new Error(`invalid full segment method ${method}`);
1033
+ }
1034
+ }
1035
+
994
1036
  // This file is inserted as a shim for modules which we do not want to include into the distro.
995
1037
  // This replacement is done in the "alias" plugin of the rollup config.
996
1038
  var empty = undefined;
997
- var Cues = /*@__PURE__*/getDefaultExportFromCjs(empty);
1039
+ var HevcVideoParser = /*@__PURE__*/getDefaultExportFromCjs(empty);
998
1040
 
999
1041
  function sliceUint8(array, start, end) {
1000
1042
  // @ts-expect-error This polyfills IE11 usage of Uint8Array slice.
@@ -1626,7 +1668,7 @@ function parseStsd(stsd) {
1626
1668
  {
1627
1669
  const codecBox = findBox(sampleEntries, [fourCC])[0];
1628
1670
  const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0];
1629
- if (esdsBox && esdsBox.length > 12) {
1671
+ if (esdsBox && esdsBox.length > 7) {
1630
1672
  let i = 4;
1631
1673
  // ES Descriptor tag
1632
1674
  if (esdsBox[i++] !== 0x03) {
@@ -1741,7 +1783,9 @@ function parseStsd(stsd) {
1741
1783
  }
1742
1784
  function skipBERInteger(bytes, i) {
1743
1785
  const limit = i + 5;
1744
- while (bytes[i++] & 0x80 && i < limit) {}
1786
+ while (bytes[i++] & 0x80 && i < limit) {
1787
+ /* do nothing */
1788
+ }
1745
1789
  return i;
1746
1790
  }
1747
1791
  function toHex(x) {
@@ -2433,12 +2477,12 @@ class LevelKey {
2433
2477
  this.keyFormatVersions = formatversions;
2434
2478
  this.iv = iv;
2435
2479
  this.encrypted = method ? method !== 'NONE' : false;
2436
- this.isCommonEncryption = this.encrypted && method !== 'AES-128';
2480
+ this.isCommonEncryption = this.encrypted && !isFullSegmentEncryption(method);
2437
2481
  }
2438
2482
  isSupported() {
2439
2483
  // If it's Segment encryption or No encryption, just select that key system
2440
2484
  if (this.method) {
2441
- if (this.method === 'AES-128' || this.method === 'NONE') {
2485
+ if (isFullSegmentEncryption(this.method) || this.method === 'NONE') {
2442
2486
  return true;
2443
2487
  }
2444
2488
  if (this.keyFormat === 'identity') {
@@ -2452,14 +2496,13 @@ class LevelKey {
2452
2496
  if (!this.encrypted || !this.uri) {
2453
2497
  return null;
2454
2498
  }
2455
- if (this.method === 'AES-128' && this.uri && !this.iv) {
2499
+ if (isFullSegmentEncryption(this.method) && this.uri && !this.iv) {
2456
2500
  if (typeof sn !== 'number') {
2457
2501
  // We are fetching decryption data for a initialization segment
2458
- // If the segment was encrypted with AES-128
2502
+ // If the segment was encrypted with AES-128/256
2459
2503
  // It must have an IV defined. We cannot substitute the Segment Number in.
2460
- if (this.method === 'AES-128' && !this.iv) {
2461
- logger.warn(`missing IV for initialization segment with method="${this.method}" - compliance issue`);
2462
- }
2504
+ logger.warn(`missing IV for initialization segment with method="${this.method}" - compliance issue`);
2505
+
2463
2506
  // Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
2464
2507
  sn = 0;
2465
2508
  }
@@ -2606,23 +2649,28 @@ function getCodecCompatibleNameLower(lowerCaseCodec, preferManagedMediaSource =
2606
2649
  if (CODEC_COMPATIBLE_NAMES[lowerCaseCodec]) {
2607
2650
  return CODEC_COMPATIBLE_NAMES[lowerCaseCodec];
2608
2651
  }
2609
-
2610
- // Idealy fLaC and Opus would be first (spec-compliant) but
2611
- // some browsers will report that fLaC is supported then fail.
2612
- // see: https://bugs.chromium.org/p/chromium/issues/detail?id=1422728
2613
2652
  const codecsToCheck = {
2653
+ // Idealy fLaC and Opus would be first (spec-compliant) but
2654
+ // some browsers will report that fLaC is supported then fail.
2655
+ // see: https://bugs.chromium.org/p/chromium/issues/detail?id=1422728
2614
2656
  flac: ['flac', 'fLaC', 'FLAC'],
2615
- opus: ['opus', 'Opus']
2657
+ opus: ['opus', 'Opus'],
2658
+ // Replace audio codec info if browser does not support mp4a.40.34,
2659
+ // and demuxer can fallback to 'audio/mpeg' or 'audio/mp4;codecs="mp3"'
2660
+ 'mp4a.40.34': ['mp3']
2616
2661
  }[lowerCaseCodec];
2617
2662
  for (let i = 0; i < codecsToCheck.length; i++) {
2663
+ var _getMediaSource;
2618
2664
  if (isCodecMediaSourceSupported(codecsToCheck[i], 'audio', preferManagedMediaSource)) {
2619
2665
  CODEC_COMPATIBLE_NAMES[lowerCaseCodec] = codecsToCheck[i];
2620
2666
  return codecsToCheck[i];
2667
+ } else if (codecsToCheck[i] === 'mp3' && (_getMediaSource = getMediaSource(preferManagedMediaSource)) != null && _getMediaSource.isTypeSupported('audio/mpeg')) {
2668
+ return '';
2621
2669
  }
2622
2670
  }
2623
2671
  return lowerCaseCodec;
2624
2672
  }
2625
- const AUDIO_CODEC_REGEXP = /flac|opus/i;
2673
+ const AUDIO_CODEC_REGEXP = /flac|opus|mp4a\.40\.34/i;
2626
2674
  function getCodecCompatibleName(codec, preferManagedMediaSource = true) {
2627
2675
  return codec.replace(AUDIO_CODEC_REGEXP, m => getCodecCompatibleNameLower(m.toLowerCase(), preferManagedMediaSource));
2628
2676
  }
@@ -2645,6 +2693,16 @@ function convertAVC1ToAVCOTI(codec) {
2645
2693
  }
2646
2694
  return codec;
2647
2695
  }
2696
+ function getM2TSSupportedAudioTypes(preferManagedMediaSource) {
2697
+ const MediaSource = getMediaSource(preferManagedMediaSource) || {
2698
+ isTypeSupported: () => false
2699
+ };
2700
+ return {
2701
+ mpeg: MediaSource.isTypeSupported('audio/mpeg'),
2702
+ mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
2703
+ ac3: false
2704
+ };
2705
+ }
2648
2706
 
2649
2707
  const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g;
2650
2708
  const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
@@ -3445,10 +3503,10 @@ class PlaylistLoader {
3445
3503
  const loaderContext = loader.context;
3446
3504
  if (loaderContext && loaderContext.url === context.url && loaderContext.level === context.level) {
3447
3505
  // same URL can't overlap
3448
- logger.trace('[playlist-loader]: playlist request ongoing');
3506
+ this.hls.logger.trace('[playlist-loader]: playlist request ongoing');
3449
3507
  return;
3450
3508
  }
3451
- logger.log(`[playlist-loader]: aborting previous loader for type: ${context.type}`);
3509
+ this.hls.logger.log(`[playlist-loader]: aborting previous loader for type: ${context.type}`);
3452
3510
  loader.abort();
3453
3511
  }
3454
3512
 
@@ -3558,7 +3616,7 @@ class PlaylistLoader {
3558
3616
  // alt audio rendition in which quality levels (main)
3559
3617
  // contains both audio+video. but with mixed audio track not signaled
3560
3618
  if (!embeddedAudioFound && levels[0].audioCodec && !levels[0].attrs.AUDIO) {
3561
- logger.log('[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one');
3619
+ this.hls.logger.log('[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one');
3562
3620
  audioTracks.unshift({
3563
3621
  type: 'main',
3564
3622
  name: 'main',
@@ -3657,7 +3715,7 @@ class PlaylistLoader {
3657
3715
  message += ` id: ${context.id} group-id: "${context.groupId}"`;
3658
3716
  }
3659
3717
  const error = new Error(message);
3660
- logger.warn(`[playlist-loader]: ${message}`);
3718
+ this.hls.logger.warn(`[playlist-loader]: ${message}`);
3661
3719
  let details = ErrorDetails.UNKNOWN;
3662
3720
  let fatal = false;
3663
3721
  const loader = this.getInternalLoader(context);
@@ -4222,7 +4280,47 @@ class LatencyController {
4222
4280
  this.currentTime = 0;
4223
4281
  this.stallCount = 0;
4224
4282
  this._latency = null;
4225
- this.timeupdateHandler = () => this.timeupdate();
4283
+ this.onTimeupdate = () => {
4284
+ const {
4285
+ media,
4286
+ levelDetails
4287
+ } = this;
4288
+ if (!media || !levelDetails) {
4289
+ return;
4290
+ }
4291
+ this.currentTime = media.currentTime;
4292
+ const latency = this.computeLatency();
4293
+ if (latency === null) {
4294
+ return;
4295
+ }
4296
+ this._latency = latency;
4297
+
4298
+ // Adapt playbackRate to meet target latency in low-latency mode
4299
+ const {
4300
+ lowLatencyMode,
4301
+ maxLiveSyncPlaybackRate
4302
+ } = this.config;
4303
+ if (!lowLatencyMode || maxLiveSyncPlaybackRate === 1 || !levelDetails.live) {
4304
+ return;
4305
+ }
4306
+ const targetLatency = this.targetLatency;
4307
+ if (targetLatency === null) {
4308
+ return;
4309
+ }
4310
+ const distanceFromTarget = latency - targetLatency;
4311
+ // Only adjust playbackRate when within one target duration of targetLatency
4312
+ // and more than one second from under-buffering.
4313
+ // Playback further than one target duration from target can be considered DVR playback.
4314
+ const liveMinLatencyDuration = Math.min(this.maxLatency, targetLatency + levelDetails.targetduration);
4315
+ const inLiveRange = distanceFromTarget < liveMinLatencyDuration;
4316
+ if (inLiveRange && distanceFromTarget > 0.05 && this.forwardBufferLength > 1) {
4317
+ const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate));
4318
+ const rate = Math.round(2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled)) * 20) / 20;
4319
+ media.playbackRate = Math.min(max, Math.max(1, rate));
4320
+ } else if (media.playbackRate !== 1 && media.playbackRate !== 0) {
4321
+ media.playbackRate = 1;
4322
+ }
4323
+ };
4226
4324
  this.hls = hls;
4227
4325
  this.config = hls.config;
4228
4326
  this.registerListeners();
@@ -4314,7 +4412,7 @@ class LatencyController {
4314
4412
  this.onMediaDetaching();
4315
4413
  this.levelDetails = null;
4316
4414
  // @ts-ignore
4317
- this.hls = this.timeupdateHandler = null;
4415
+ this.hls = null;
4318
4416
  }
4319
4417
  registerListeners() {
4320
4418
  this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
@@ -4332,11 +4430,11 @@ class LatencyController {
4332
4430
  }
4333
4431
  onMediaAttached(event, data) {
4334
4432
  this.media = data.media;
4335
- this.media.addEventListener('timeupdate', this.timeupdateHandler);
4433
+ this.media.addEventListener('timeupdate', this.onTimeupdate);
4336
4434
  }
4337
4435
  onMediaDetaching() {
4338
4436
  if (this.media) {
4339
- this.media.removeEventListener('timeupdate', this.timeupdateHandler);
4437
+ this.media.removeEventListener('timeupdate', this.onTimeupdate);
4340
4438
  this.media = null;
4341
4439
  }
4342
4440
  }
@@ -4350,10 +4448,10 @@ class LatencyController {
4350
4448
  }) {
4351
4449
  this.levelDetails = details;
4352
4450
  if (details.advanced) {
4353
- this.timeupdate();
4451
+ this.onTimeupdate();
4354
4452
  }
4355
4453
  if (!details.live && this.media) {
4356
- this.media.removeEventListener('timeupdate', this.timeupdateHandler);
4454
+ this.media.removeEventListener('timeupdate', this.onTimeupdate);
4357
4455
  }
4358
4456
  }
4359
4457
  onError(event, data) {
@@ -4363,48 +4461,7 @@ class LatencyController {
4363
4461
  }
4364
4462
  this.stallCount++;
4365
4463
  if ((_this$levelDetails = this.levelDetails) != null && _this$levelDetails.live) {
4366
- logger.warn('[playback-rate-controller]: Stall detected, adjusting target latency');
4367
- }
4368
- }
4369
- timeupdate() {
4370
- const {
4371
- media,
4372
- levelDetails
4373
- } = this;
4374
- if (!media || !levelDetails) {
4375
- return;
4376
- }
4377
- this.currentTime = media.currentTime;
4378
- const latency = this.computeLatency();
4379
- if (latency === null) {
4380
- return;
4381
- }
4382
- this._latency = latency;
4383
-
4384
- // Adapt playbackRate to meet target latency in low-latency mode
4385
- const {
4386
- lowLatencyMode,
4387
- maxLiveSyncPlaybackRate
4388
- } = this.config;
4389
- if (!lowLatencyMode || maxLiveSyncPlaybackRate === 1 || !levelDetails.live) {
4390
- return;
4391
- }
4392
- const targetLatency = this.targetLatency;
4393
- if (targetLatency === null) {
4394
- return;
4395
- }
4396
- const distanceFromTarget = latency - targetLatency;
4397
- // Only adjust playbackRate when within one target duration of targetLatency
4398
- // and more than one second from under-buffering.
4399
- // Playback further than one target duration from target can be considered DVR playback.
4400
- const liveMinLatencyDuration = Math.min(this.maxLatency, targetLatency + levelDetails.targetduration);
4401
- const inLiveRange = distanceFromTarget < liveMinLatencyDuration;
4402
- if (inLiveRange && distanceFromTarget > 0.05 && this.forwardBufferLength > 1) {
4403
- const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate));
4404
- const rate = Math.round(2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled)) * 20) / 20;
4405
- media.playbackRate = Math.min(max, Math.max(1, rate));
4406
- } else if (media.playbackRate !== 1 && media.playbackRate !== 0) {
4407
- media.playbackRate = 1;
4464
+ this.hls.logger.warn('[latency-controller]: Stall detected, adjusting target latency');
4408
4465
  }
4409
4466
  }
4410
4467
  estimateLiveEdge() {
@@ -5176,18 +5233,13 @@ var ErrorActionFlags = {
5176
5233
  MoveAllAlternatesMatchingHDCP: 2,
5177
5234
  SwitchToSDR: 4
5178
5235
  }; // Reserved for future use
5179
- class ErrorController {
5236
+ class ErrorController extends Logger {
5180
5237
  constructor(hls) {
5238
+ super('error-controller', hls.logger);
5181
5239
  this.hls = void 0;
5182
5240
  this.playlistError = 0;
5183
5241
  this.penalizedRenditions = {};
5184
- this.log = void 0;
5185
- this.warn = void 0;
5186
- this.error = void 0;
5187
5242
  this.hls = hls;
5188
- this.log = logger.log.bind(logger, `[info]:`);
5189
- this.warn = logger.warn.bind(logger, `[warning]:`);
5190
- this.error = logger.error.bind(logger, `[error]:`);
5191
5243
  this.registerListeners();
5192
5244
  }
5193
5245
  registerListeners() {
@@ -5539,16 +5591,13 @@ class ErrorController {
5539
5591
  }
5540
5592
  }
5541
5593
 
5542
- class BasePlaylistController {
5594
+ class BasePlaylistController extends Logger {
5543
5595
  constructor(hls, logPrefix) {
5596
+ super(logPrefix, hls.logger);
5544
5597
  this.hls = void 0;
5545
5598
  this.timer = -1;
5546
5599
  this.requestScheduled = -1;
5547
5600
  this.canLoad = false;
5548
- this.log = void 0;
5549
- this.warn = void 0;
5550
- this.log = logger.log.bind(logger, `${logPrefix}:`);
5551
- this.warn = logger.warn.bind(logger, `${logPrefix}:`);
5552
5601
  this.hls = hls;
5553
5602
  }
5554
5603
  destroy() {
@@ -5581,7 +5630,7 @@ class BasePlaylistController {
5581
5630
  try {
5582
5631
  uri = new self.URL(attr.URI, previous.url).href;
5583
5632
  } catch (error) {
5584
- logger.warn(`Could not construct new URL for Rendition Report: ${error}`);
5633
+ this.warn(`Could not construct new URL for Rendition Report: ${error}`);
5585
5634
  uri = attr.URI || '';
5586
5635
  }
5587
5636
  // Use exact match. Otherwise, the last partial match, if any, will be used
@@ -5668,7 +5717,12 @@ class BasePlaylistController {
5668
5717
  const cdnAge = lastAdvanced + details.ageHeader;
5669
5718
  let currentGoal = Math.min(cdnAge - details.partTarget, details.targetduration * 1.5);
5670
5719
  if (currentGoal > 0) {
5671
- if (previousDetails && currentGoal > previousDetails.tuneInGoal) {
5720
+ if (cdnAge > details.targetduration * 3) {
5721
+ // Omit segment and part directives when the last response was more than 3 target durations ago,
5722
+ this.log(`Playlist last advanced ${lastAdvanced.toFixed(2)}s ago. Omitting segment and part directives.`);
5723
+ msn = undefined;
5724
+ part = undefined;
5725
+ } else if (previousDetails != null && previousDetails.tuneInGoal && cdnAge - details.partTarget > previousDetails.tuneInGoal) {
5672
5726
  // If we attempted to get the next or latest playlist update, but currentGoal increased,
5673
5727
  // then we either can't catchup, or the "age" header cannot be trusted.
5674
5728
  this.warn(`CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`);
@@ -6127,8 +6181,9 @@ function getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel) {
6127
6181
  }, {});
6128
6182
  }
6129
6183
 
6130
- class AbrController {
6184
+ class AbrController extends Logger {
6131
6185
  constructor(_hls) {
6186
+ super('abr', _hls.logger);
6132
6187
  this.hls = void 0;
6133
6188
  this.lastLevelLoadSec = 0;
6134
6189
  this.lastLoadedFragLevel = -1;
@@ -6242,7 +6297,7 @@ class AbrController {
6242
6297
  this.resetEstimator(nextLoadLevelBitrate);
6243
6298
  }
6244
6299
  this.clearTimer();
6245
- logger.warn(`[abr] Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${frag.level} is loading too slowly;
6300
+ this.warn(`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${frag.level} is loading too slowly;
6246
6301
  Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s
6247
6302
  Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s
6248
6303
  Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(3)} s
@@ -6262,7 +6317,7 @@ class AbrController {
6262
6317
  }
6263
6318
  resetEstimator(abrEwmaDefaultEstimate) {
6264
6319
  if (abrEwmaDefaultEstimate) {
6265
- logger.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
6320
+ this.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
6266
6321
  this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate;
6267
6322
  }
6268
6323
  this.firstSelection = -1;
@@ -6494,7 +6549,7 @@ class AbrController {
6494
6549
  }
6495
6550
  const firstLevel = this.hls.firstLevel;
6496
6551
  const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel);
6497
- logger.warn(`[abr] Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`);
6552
+ this.warn(`Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`);
6498
6553
  return clamped;
6499
6554
  }
6500
6555
  get forcedAutoLevel() {
@@ -6540,6 +6595,9 @@ class AbrController {
6540
6595
  partCurrent,
6541
6596
  hls
6542
6597
  } = this;
6598
+ if (hls.levels.length <= 1) {
6599
+ return hls.loadLevel;
6600
+ }
6543
6601
  const {
6544
6602
  maxAutoLevel,
6545
6603
  config,
@@ -6572,13 +6630,13 @@ class AbrController {
6572
6630
  // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
6573
6631
  const maxLoadingDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxLoadingDelay) : config.maxLoadingDelay;
6574
6632
  maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
6575
- logger.info(`[abr] bitrate test took ${Math.round(1000 * bitrateTestDelay)}ms, set first fragment max fetchDuration to ${Math.round(1000 * maxStarvationDelay)} ms`);
6633
+ this.info(`bitrate test took ${Math.round(1000 * bitrateTestDelay)}ms, set first fragment max fetchDuration to ${Math.round(1000 * maxStarvationDelay)} ms`);
6576
6634
  // don't use conservative factor on bitrate test
6577
6635
  bwFactor = bwUpFactor = 1;
6578
6636
  }
6579
6637
  }
6580
6638
  const bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor);
6581
- logger.info(`[abr] ${bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'}, optimal quality level ${bestLevel}`);
6639
+ this.info(`${bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'}, optimal quality level ${bestLevel}`);
6582
6640
  if (bestLevel > -1) {
6583
6641
  return bestLevel;
6584
6642
  }
@@ -6652,7 +6710,7 @@ class AbrController {
6652
6710
  currentVideoRange = preferHDR ? videoRanges[videoRanges.length - 1] : videoRanges[0];
6653
6711
  currentFrameRate = minFramerate;
6654
6712
  currentBw = Math.max(currentBw, minBitrate);
6655
- logger.log(`[abr] picked start tier ${JSON.stringify(startTier)}`);
6713
+ this.log(`picked start tier ${JSON.stringify(startTier)}`);
6656
6714
  } else {
6657
6715
  currentCodecSet = level == null ? void 0 : level.codecSet;
6658
6716
  currentVideoRange = level == null ? void 0 : level.videoRange;
@@ -6705,9 +6763,9 @@ class AbrController {
6705
6763
  const forcedAutoLevel = this.forcedAutoLevel;
6706
6764
  if (i !== loadLevel && (forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)) {
6707
6765
  if (levelsSkipped.length) {
6708
- logger.trace(`[abr] Skipped level(s) ${levelsSkipped.join(',')} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${levels[levelsSkipped[0]].codecs}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${level.codecs}" ${currentVideoRange}`);
6766
+ this.trace(`Skipped level(s) ${levelsSkipped.join(',')} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${levels[levelsSkipped[0]].codecs}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${level.codecs}" ${currentVideoRange}`);
6709
6767
  }
6710
- logger.info(`[abr] switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(adjustedbw)})-bitrate=${Math.round(adjustedbw - bitrate)} ttfb:${ttfbEstimateSec.toFixed(1)} avgDuration:${avgDuration.toFixed(1)} maxFetchDuration:${maxFetchDuration.toFixed(1)} fetchDuration:${fetchDuration.toFixed(1)} firstSelection:${firstSelection} codecSet:${currentCodecSet} videoRange:${currentVideoRange} hls.loadLevel:${loadLevel}`);
6768
+ this.info(`switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(adjustedbw)})-bitrate=${Math.round(adjustedbw - bitrate)} ttfb:${ttfbEstimateSec.toFixed(1)} avgDuration:${avgDuration.toFixed(1)} maxFetchDuration:${maxFetchDuration.toFixed(1)} fetchDuration:${fetchDuration.toFixed(1)} firstSelection:${firstSelection} codecSet:${currentCodecSet} videoRange:${currentVideoRange} hls.loadLevel:${loadLevel}`);
6711
6769
  }
6712
6770
  if (firstSelection) {
6713
6771
  this.firstSelection = i;
@@ -6750,40 +6808,29 @@ class BufferHelper {
6750
6808
  * Return true if `media`'s buffered include `position`
6751
6809
  */
6752
6810
  static isBuffered(media, position) {
6753
- try {
6754
- if (media) {
6755
- const buffered = BufferHelper.getBuffered(media);
6756
- for (let i = 0; i < buffered.length; i++) {
6757
- if (position >= buffered.start(i) && position <= buffered.end(i)) {
6758
- return true;
6759
- }
6811
+ if (media) {
6812
+ const buffered = BufferHelper.getBuffered(media);
6813
+ for (let i = buffered.length; i--;) {
6814
+ if (position >= buffered.start(i) && position <= buffered.end(i)) {
6815
+ return true;
6760
6816
  }
6761
6817
  }
6762
- } catch (error) {
6763
- // this is to catch
6764
- // InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
6765
- // This SourceBuffer has been removed from the parent media source
6766
6818
  }
6767
6819
  return false;
6768
6820
  }
6769
6821
  static bufferInfo(media, pos, maxHoleDuration) {
6770
- try {
6771
- if (media) {
6772
- const vbuffered = BufferHelper.getBuffered(media);
6822
+ if (media) {
6823
+ const vbuffered = BufferHelper.getBuffered(media);
6824
+ if (vbuffered.length) {
6773
6825
  const buffered = [];
6774
- let i;
6775
- for (i = 0; i < vbuffered.length; i++) {
6826
+ for (let i = 0; i < vbuffered.length; i++) {
6776
6827
  buffered.push({
6777
6828
  start: vbuffered.start(i),
6778
6829
  end: vbuffered.end(i)
6779
6830
  });
6780
6831
  }
6781
- return this.bufferedInfo(buffered, pos, maxHoleDuration);
6832
+ return BufferHelper.bufferedInfo(buffered, pos, maxHoleDuration);
6782
6833
  }
6783
- } catch (error) {
6784
- // this is to catch
6785
- // InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
6786
- // This SourceBuffer has been removed from the parent media source
6787
6834
  }
6788
6835
  return {
6789
6836
  len: 0,
@@ -6795,14 +6842,7 @@ class BufferHelper {
6795
6842
  static bufferedInfo(buffered, pos, maxHoleDuration) {
6796
6843
  pos = Math.max(0, pos);
6797
6844
  // sort on buffer.start/smaller end (IE does not always return sorted buffered range)
6798
- buffered.sort(function (a, b) {
6799
- const diff = a.start - b.start;
6800
- if (diff) {
6801
- return diff;
6802
- } else {
6803
- return b.end - a.end;
6804
- }
6805
- });
6845
+ buffered.sort((a, b) => a.start - b.start || b.end - a.end);
6806
6846
  let buffered2 = [];
6807
6847
  if (maxHoleDuration) {
6808
6848
  // there might be some small holes between buffer time range
@@ -6869,7 +6909,7 @@ class BufferHelper {
6869
6909
  */
6870
6910
  static getBuffered(media) {
6871
6911
  try {
6872
- return media.buffered;
6912
+ return media.buffered || noopBuffered;
6873
6913
  } catch (e) {
6874
6914
  logger.log('failed to get media.buffered', e);
6875
6915
  return noopBuffered;
@@ -6894,24 +6934,22 @@ class BufferOperationQueue {
6894
6934
  this.executeNext(type);
6895
6935
  }
6896
6936
  }
6897
- insertAbort(operation, type) {
6898
- const queue = this.queues[type];
6899
- queue.unshift(operation);
6900
- this.executeNext(type);
6901
- }
6902
6937
  appendBlocker(type) {
6903
- let execute;
6904
- const promise = new Promise(resolve => {
6905
- execute = resolve;
6938
+ return new Promise(resolve => {
6939
+ const operation = {
6940
+ execute: resolve,
6941
+ onStart: () => {},
6942
+ onComplete: () => {},
6943
+ onError: () => {}
6944
+ };
6945
+ this.append(operation, type);
6906
6946
  });
6907
- const operation = {
6908
- execute,
6909
- onStart: () => {},
6910
- onComplete: () => {},
6911
- onError: () => {}
6912
- };
6913
- this.append(operation, type);
6914
- return promise;
6947
+ }
6948
+ unblockAudio(op) {
6949
+ const queue = this.queues.audio;
6950
+ if (queue[0] === op) {
6951
+ this.shiftAndExecuteNext('audio');
6952
+ }
6915
6953
  }
6916
6954
  executeNext(type) {
6917
6955
  const queue = this.queues[type];
@@ -6943,8 +6981,9 @@ class BufferOperationQueue {
6943
6981
  }
6944
6982
 
6945
6983
  const VIDEO_CODEC_PROFILE_REPLACE = /(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/;
6946
- class BufferController {
6947
- constructor(hls) {
6984
+ class BufferController extends Logger {
6985
+ constructor(hls, fragmentTracker) {
6986
+ super('buffer-controller', hls.logger);
6948
6987
  // The level details used to determine duration, target-duration and live
6949
6988
  this.details = null;
6950
6989
  // cache the self generated object url to detect hijack of video tag
@@ -6954,6 +6993,7 @@ class BufferController {
6954
6993
  // References to event listeners for each SourceBuffer, so that they can be referenced for event removal
6955
6994
  this.listeners = void 0;
6956
6995
  this.hls = void 0;
6996
+ this.fragmentTracker = void 0;
6957
6997
  // The number of BUFFER_CODEC events received before any sourceBuffers are created
6958
6998
  this.bufferCodecEventsExpected = 0;
6959
6999
  // The total number of BUFFER_CODEC events received
@@ -6964,6 +7004,10 @@ class BufferController {
6964
7004
  this.mediaSource = null;
6965
7005
  // Last MP3 audio chunk appended
6966
7006
  this.lastMpegAudioChunk = null;
7007
+ // Audio fragment blocked from appending until corresponding video appends or context changes
7008
+ this.blockedAudioAppend = null;
7009
+ // Keep track of video append position for unblocking audio
7010
+ this.lastVideoAppendEnd = 0;
6967
7011
  this.appendSource = void 0;
6968
7012
  // counters
6969
7013
  this.appendErrors = {
@@ -6974,9 +7018,6 @@ class BufferController {
6974
7018
  this.tracks = {};
6975
7019
  this.pendingTracks = {};
6976
7020
  this.sourceBuffer = void 0;
6977
- this.log = void 0;
6978
- this.warn = void 0;
6979
- this.error = void 0;
6980
7021
  this._onEndStreaming = event => {
6981
7022
  if (!this.hls) {
6982
7023
  return;
@@ -6998,7 +7039,10 @@ class BufferController {
6998
7039
  this.log('Media source opened');
6999
7040
  if (media) {
7000
7041
  media.removeEventListener('emptied', this._onMediaEmptied);
7001
- this.updateMediaElementDuration();
7042
+ const durationAndRange = this.getDurationAndRange();
7043
+ if (durationAndRange) {
7044
+ this.updateMediaSource(durationAndRange);
7045
+ }
7002
7046
  this.hls.trigger(Events.MEDIA_ATTACHED, {
7003
7047
  media,
7004
7048
  mediaSource: mediaSource
@@ -7022,15 +7066,12 @@ class BufferController {
7022
7066
  _objectUrl
7023
7067
  } = this;
7024
7068
  if (mediaSrc !== _objectUrl) {
7025
- logger.error(`Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`);
7069
+ this.error(`Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`);
7026
7070
  }
7027
7071
  };
7028
7072
  this.hls = hls;
7029
- const logPrefix = '[buffer-controller]';
7073
+ this.fragmentTracker = fragmentTracker;
7030
7074
  this.appendSource = hls.config.preferManagedMediaSource && typeof self !== 'undefined' && self.ManagedMediaSource;
7031
- this.log = logger.log.bind(logger, logPrefix);
7032
- this.warn = logger.warn.bind(logger, logPrefix);
7033
- this.error = logger.error.bind(logger, logPrefix);
7034
7075
  this._initSourceBuffer();
7035
7076
  this.registerListeners();
7036
7077
  }
@@ -7042,7 +7083,13 @@ class BufferController {
7042
7083
  this.details = null;
7043
7084
  this.lastMpegAudioChunk = null;
7044
7085
  // @ts-ignore
7045
- this.hls = null;
7086
+ this.hls = this.fragmentTracker = null;
7087
+ // @ts-ignore
7088
+ this._onMediaSourceOpen = this._onMediaSourceClose = null;
7089
+ // @ts-ignore
7090
+ this._onMediaSourceEnded = null;
7091
+ // @ts-ignore
7092
+ this._onStartStreaming = this._onEndStreaming = null;
7046
7093
  }
7047
7094
  registerListeners() {
7048
7095
  const {
@@ -7092,6 +7139,8 @@ class BufferController {
7092
7139
  audiovideo: 0
7093
7140
  };
7094
7141
  this.lastMpegAudioChunk = null;
7142
+ this.blockedAudioAppend = null;
7143
+ this.lastVideoAppendEnd = 0;
7095
7144
  }
7096
7145
  onManifestLoading() {
7097
7146
  this.bufferCodecEventsExpected = this._bufferCodecEventsTotal = 0;
@@ -7209,6 +7258,7 @@ class BufferController {
7209
7258
  this.resetBuffer(type);
7210
7259
  });
7211
7260
  this._initSourceBuffer();
7261
+ this.hls.resumeBuffering();
7212
7262
  }
7213
7263
  resetBuffer(type) {
7214
7264
  const sb = this.sourceBuffer[type];
@@ -7232,9 +7282,10 @@ class BufferController {
7232
7282
  const trackNames = Object.keys(data);
7233
7283
  trackNames.forEach(trackName => {
7234
7284
  if (sourceBufferCount) {
7285
+ var _track$buffer;
7235
7286
  // check if SourceBuffer codec needs to change
7236
7287
  const track = this.tracks[trackName];
7237
- if (track && typeof track.buffer.changeType === 'function') {
7288
+ if (track && typeof ((_track$buffer = track.buffer) == null ? void 0 : _track$buffer.changeType) === 'function') {
7238
7289
  var _trackCodec;
7239
7290
  const {
7240
7291
  id,
@@ -7304,20 +7355,54 @@ class BufferController {
7304
7355
  };
7305
7356
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
7306
7357
  }
7358
+ blockAudio(partOrFrag) {
7359
+ var _this$fragmentTracker;
7360
+ const pStart = partOrFrag.start;
7361
+ const pTime = pStart + partOrFrag.duration * 0.05;
7362
+ const atGap = ((_this$fragmentTracker = this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)) == null ? void 0 : _this$fragmentTracker.gap) === true;
7363
+ if (atGap) {
7364
+ return;
7365
+ }
7366
+ const op = {
7367
+ execute: () => {
7368
+ var _this$fragmentTracker2;
7369
+ if (this.lastVideoAppendEnd > pTime || this.sourceBuffer.video && BufferHelper.isBuffered(this.sourceBuffer.video, pTime) || ((_this$fragmentTracker2 = this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)) == null ? void 0 : _this$fragmentTracker2.gap) === true) {
7370
+ this.blockedAudioAppend = null;
7371
+ this.operationQueue.shiftAndExecuteNext('audio');
7372
+ }
7373
+ },
7374
+ onStart: () => {},
7375
+ onComplete: () => {},
7376
+ onError: () => {}
7377
+ };
7378
+ this.blockedAudioAppend = {
7379
+ op,
7380
+ frag: partOrFrag
7381
+ };
7382
+ this.operationQueue.append(op, 'audio', true);
7383
+ }
7384
+ unblockAudio() {
7385
+ const blockedAudioAppend = this.blockedAudioAppend;
7386
+ if (blockedAudioAppend) {
7387
+ this.blockedAudioAppend = null;
7388
+ this.operationQueue.unblockAudio(blockedAudioAppend.op);
7389
+ }
7390
+ }
7307
7391
  onBufferAppending(event, eventData) {
7308
7392
  const {
7309
- hls,
7310
7393
  operationQueue,
7311
7394
  tracks
7312
7395
  } = this;
7313
7396
  const {
7314
7397
  data,
7315
7398
  type,
7399
+ parent,
7316
7400
  frag,
7317
7401
  part,
7318
7402
  chunkMeta
7319
7403
  } = eventData;
7320
7404
  const chunkStats = chunkMeta.buffering[type];
7405
+ const sn = frag.sn;
7321
7406
  const bufferAppendingStart = self.performance.now();
7322
7407
  chunkStats.start = bufferAppendingStart;
7323
7408
  const fragBuffering = frag.stats.buffering;
@@ -7340,7 +7425,36 @@ class BufferController {
7340
7425
  checkTimestampOffset = !this.lastMpegAudioChunk || chunkMeta.id === 1 || this.lastMpegAudioChunk.sn !== chunkMeta.sn;
7341
7426
  this.lastMpegAudioChunk = chunkMeta;
7342
7427
  }
7343
- const fragStart = frag.start;
7428
+
7429
+ // Block audio append until overlapping video append
7430
+ const videoSb = this.sourceBuffer.video;
7431
+ if (videoSb && sn !== 'initSegment') {
7432
+ const partOrFrag = part || frag;
7433
+ const blockedAudioAppend = this.blockedAudioAppend;
7434
+ if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) {
7435
+ const pStart = partOrFrag.start;
7436
+ const pTime = pStart + partOrFrag.duration * 0.05;
7437
+ const vbuffered = videoSb.buffered;
7438
+ const vappending = this.operationQueue.current('video');
7439
+ if (!vbuffered.length && !vappending) {
7440
+ // wait for video before appending audio
7441
+ this.blockAudio(partOrFrag);
7442
+ } else if (!vappending && !BufferHelper.isBuffered(videoSb, pTime) && this.lastVideoAppendEnd < pTime) {
7443
+ // audio is ahead of video
7444
+ this.blockAudio(partOrFrag);
7445
+ }
7446
+ } else if (type === 'video') {
7447
+ const videoAppendEnd = partOrFrag.end;
7448
+ if (blockedAudioAppend) {
7449
+ const audioStart = blockedAudioAppend.frag.start;
7450
+ if (videoAppendEnd > audioStart || videoAppendEnd < this.lastVideoAppendEnd || BufferHelper.isBuffered(videoSb, audioStart)) {
7451
+ this.unblockAudio();
7452
+ }
7453
+ }
7454
+ this.lastVideoAppendEnd = videoAppendEnd;
7455
+ }
7456
+ }
7457
+ const fragStart = (part || frag).start;
7344
7458
  const operation = {
7345
7459
  execute: () => {
7346
7460
  chunkStats.executeStart = self.performance.now();
@@ -7349,7 +7463,7 @@ class BufferController {
7349
7463
  if (sb) {
7350
7464
  const delta = fragStart - sb.timestampOffset;
7351
7465
  if (Math.abs(delta) >= 0.1) {
7352
- this.log(`Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`);
7466
+ this.log(`Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${sn})`);
7353
7467
  sb.timestampOffset = fragStart;
7354
7468
  }
7355
7469
  }
@@ -7416,22 +7530,21 @@ class BufferController {
7416
7530
  /* with UHD content, we could get loop of quota exceeded error until
7417
7531
  browser is able to evict some data from sourcebuffer. Retrying can help recover.
7418
7532
  */
7419
- this.warn(`Failed ${appendErrorCount}/${hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`);
7420
- if (appendErrorCount >= hls.config.appendErrorMaxRetry) {
7533
+ this.warn(`Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`);
7534
+ if (appendErrorCount >= this.hls.config.appendErrorMaxRetry) {
7421
7535
  event.fatal = true;
7422
7536
  }
7423
7537
  }
7424
- hls.trigger(Events.ERROR, event);
7538
+ this.hls.trigger(Events.ERROR, event);
7425
7539
  }
7426
7540
  };
7427
7541
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
7428
7542
  }
7429
- onBufferFlushing(event, data) {
7430
- const {
7431
- operationQueue
7432
- } = this;
7433
- const flushOperation = type => ({
7434
- execute: this.removeExecutor.bind(this, type, data.startOffset, data.endOffset),
7543
+ getFlushOp(type, start, end) {
7544
+ return {
7545
+ execute: () => {
7546
+ this.removeExecutor(type, start, end);
7547
+ },
7435
7548
  onStart: () => {
7436
7549
  // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
7437
7550
  },
@@ -7444,12 +7557,22 @@ class BufferController {
7444
7557
  onError: error => {
7445
7558
  this.warn(`Failed to remove from ${type} SourceBuffer`, error);
7446
7559
  }
7447
- });
7448
- if (data.type) {
7449
- operationQueue.append(flushOperation(data.type), data.type);
7560
+ };
7561
+ }
7562
+ onBufferFlushing(event, data) {
7563
+ const {
7564
+ operationQueue
7565
+ } = this;
7566
+ const {
7567
+ type,
7568
+ startOffset,
7569
+ endOffset
7570
+ } = data;
7571
+ if (type) {
7572
+ operationQueue.append(this.getFlushOp(type, startOffset, endOffset), type);
7450
7573
  } else {
7451
- this.getSourceBufferTypes().forEach(type => {
7452
- operationQueue.append(flushOperation(type), type);
7574
+ this.getSourceBufferTypes().forEach(sbType => {
7575
+ operationQueue.append(this.getFlushOp(sbType, startOffset, endOffset), sbType);
7453
7576
  });
7454
7577
  }
7455
7578
  }
@@ -7496,6 +7619,9 @@ class BufferController {
7496
7619
  // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
7497
7620
  // an undefined data.type will mark all buffers as EOS.
7498
7621
  onBufferEos(event, data) {
7622
+ if (data.type === 'video') {
7623
+ this.unblockAudio();
7624
+ }
7499
7625
  const ended = this.getSourceBufferTypes().reduce((acc, type) => {
7500
7626
  const sb = this.sourceBuffer[type];
7501
7627
  if (sb && (!data.type || data.type === type)) {
@@ -7538,10 +7664,14 @@ class BufferController {
7538
7664
  return;
7539
7665
  }
7540
7666
  this.details = details;
7667
+ const durationAndRange = this.getDurationAndRange();
7668
+ if (!durationAndRange) {
7669
+ return;
7670
+ }
7541
7671
  if (this.getSourceBufferTypes().length) {
7542
- this.blockBuffers(this.updateMediaElementDuration.bind(this));
7672
+ this.blockBuffers(() => this.updateMediaSource(durationAndRange));
7543
7673
  } else {
7544
- this.updateMediaElementDuration();
7674
+ this.updateMediaSource(durationAndRange);
7545
7675
  }
7546
7676
  }
7547
7677
  trimBuffers() {
@@ -7646,9 +7776,9 @@ class BufferController {
7646
7776
  * 'liveDurationInfinity` is set to `true`
7647
7777
  * More details: https://github.com/video-dev/hls.js/issues/355
7648
7778
  */
7649
- updateMediaElementDuration() {
7779
+ getDurationAndRange() {
7650
7780
  if (!this.details || !this.media || !this.mediaSource || this.mediaSource.readyState !== 'open') {
7651
- return;
7781
+ return null;
7652
7782
  }
7653
7783
  const {
7654
7784
  details,
@@ -7662,25 +7792,41 @@ class BufferController {
7662
7792
  if (details.live && hls.config.liveDurationInfinity) {
7663
7793
  // Override duration to Infinity
7664
7794
  mediaSource.duration = Infinity;
7665
- this.updateSeekableRange(details);
7795
+ const len = details.fragments.length;
7796
+ if (len && details.live && !!mediaSource.setLiveSeekableRange) {
7797
+ const start = Math.max(0, details.fragments[0].start);
7798
+ const end = Math.max(start, start + details.totalduration);
7799
+ return {
7800
+ duration: Infinity,
7801
+ start,
7802
+ end
7803
+ };
7804
+ }
7805
+ return {
7806
+ duration: Infinity
7807
+ };
7666
7808
  } else if (levelDuration > msDuration && levelDuration > mediaDuration || !isFiniteNumber(mediaDuration)) {
7667
- // levelDuration was the last value we set.
7668
- // not using mediaSource.duration as the browser may tweak this value
7669
- // only update Media Source duration if its value increase, this is to avoid
7670
- // flushing already buffered portion when switching between quality level
7671
- this.log(`Updating Media Source duration to ${levelDuration.toFixed(3)}`);
7672
- mediaSource.duration = levelDuration;
7809
+ return {
7810
+ duration: levelDuration
7811
+ };
7673
7812
  }
7813
+ return null;
7674
7814
  }
7675
- updateSeekableRange(levelDetails) {
7676
- const mediaSource = this.mediaSource;
7677
- const fragments = levelDetails.fragments;
7678
- const len = fragments.length;
7679
- if (len && levelDetails.live && mediaSource != null && mediaSource.setLiveSeekableRange) {
7680
- const start = Math.max(0, fragments[0].start);
7681
- const end = Math.max(start, start + levelDetails.totalduration);
7682
- this.log(`Media Source duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`);
7683
- mediaSource.setLiveSeekableRange(start, end);
7815
+ updateMediaSource({
7816
+ duration,
7817
+ start,
7818
+ end
7819
+ }) {
7820
+ if (!this.media || !this.mediaSource || this.mediaSource.readyState !== 'open') {
7821
+ return;
7822
+ }
7823
+ if (isFiniteNumber(duration)) {
7824
+ this.log(`Updating Media Source duration to ${duration.toFixed(3)}`);
7825
+ }
7826
+ this.mediaSource.duration = duration;
7827
+ if (start !== undefined && end !== undefined) {
7828
+ this.log(`Media Source duration is set to ${this.mediaSource.duration}. Setting seekable range to ${start}-${end}.`);
7829
+ this.mediaSource.setLiveSeekableRange(start, end);
7684
7830
  }
7685
7831
  }
7686
7832
  checkPendingTracks() {
@@ -7865,6 +8011,7 @@ class BufferController {
7865
8011
  }
7866
8012
  return;
7867
8013
  }
8014
+ sb.ending = false;
7868
8015
  sb.ended = false;
7869
8016
  sb.appendBuffer(data);
7870
8017
  }
@@ -7884,10 +8031,14 @@ class BufferController {
7884
8031
 
7885
8032
  // logger.debug(`[buffer-controller]: Blocking ${buffers} SourceBuffer`);
7886
8033
  const blockingOperations = buffers.map(type => operationQueue.appendBlocker(type));
7887
- Promise.all(blockingOperations).then(() => {
8034
+ const audioBlocked = buffers.length > 1 && !!this.blockedAudioAppend;
8035
+ if (audioBlocked) {
8036
+ this.unblockAudio();
8037
+ }
8038
+ Promise.all(blockingOperations).then(result => {
7888
8039
  // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
7889
8040
  onUnblocked();
7890
- buffers.forEach(type => {
8041
+ buffers.forEach((type, i) => {
7891
8042
  const sb = this.sourceBuffer[type];
7892
8043
  // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
7893
8044
  // true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration)
@@ -8048,10 +8199,10 @@ class CapLevelController {
8048
8199
  const hls = this.hls;
8049
8200
  const maxLevel = this.getMaxLevel(levels.length - 1);
8050
8201
  if (maxLevel !== this.autoLevelCapping) {
8051
- logger.log(`Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`);
8202
+ hls.logger.log(`Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`);
8052
8203
  }
8053
8204
  hls.autoLevelCapping = maxLevel;
8054
- if (hls.autoLevelCapping > this.autoLevelCapping && this.streamController) {
8205
+ if (hls.autoLevelEnabled && hls.autoLevelCapping > this.autoLevelCapping && this.streamController) {
8055
8206
  // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
8056
8207
  // usually happen when the user go to the fullscreen mode.
8057
8208
  this.streamController.nextLevelSwitch();
@@ -8226,10 +8377,10 @@ class FPSController {
8226
8377
  totalDroppedFrames: droppedFrames
8227
8378
  });
8228
8379
  if (droppedFPS > 0) {
8229
- // logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
8380
+ // hls.logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
8230
8381
  if (currentDropped > hls.config.fpsDroppedMonitoringThreshold * currentDecoded) {
8231
8382
  let currentLevel = hls.currentLevel;
8232
- logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel);
8383
+ hls.logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel);
8233
8384
  if (currentLevel > 0 && (hls.autoLevelCapping === -1 || hls.autoLevelCapping >= currentLevel)) {
8234
8385
  currentLevel = currentLevel - 1;
8235
8386
  hls.trigger(Events.FPS_DROP_LEVEL_CAPPING, {
@@ -8262,10 +8413,10 @@ class FPSController {
8262
8413
  }
8263
8414
 
8264
8415
  const PATHWAY_PENALTY_DURATION_MS = 300000;
8265
- class ContentSteeringController {
8416
+ class ContentSteeringController extends Logger {
8266
8417
  constructor(hls) {
8418
+ super('content-steering', hls.logger);
8267
8419
  this.hls = void 0;
8268
- this.log = void 0;
8269
8420
  this.loader = null;
8270
8421
  this.uri = null;
8271
8422
  this.pathwayId = '.';
@@ -8280,7 +8431,6 @@ class ContentSteeringController {
8280
8431
  this.subtitleTracks = null;
8281
8432
  this.penalizedPathways = {};
8282
8433
  this.hls = hls;
8283
- this.log = logger.log.bind(logger, `[content-steering]:`);
8284
8434
  this.registerListeners();
8285
8435
  }
8286
8436
  registerListeners() {
@@ -8404,7 +8554,7 @@ class ContentSteeringController {
8404
8554
  errorAction.resolved = this.pathwayId !== errorPathway;
8405
8555
  }
8406
8556
  if (!errorAction.resolved) {
8407
- logger.warn(`Could not resolve ${data.details} ("${data.error.message}") with content-steering for Pathway: ${errorPathway} levels: ${levels ? levels.length : levels} priorities: ${JSON.stringify(pathwayPriority)} penalized: ${JSON.stringify(this.penalizedPathways)}`);
8557
+ this.warn(`Could not resolve ${data.details} ("${data.error.message}") with content-steering for Pathway: ${errorPathway} levels: ${levels ? levels.length : levels} priorities: ${JSON.stringify(pathwayPriority)} penalized: ${JSON.stringify(this.penalizedPathways)}`);
8408
8558
  }
8409
8559
  }
8410
8560
  }
@@ -8575,7 +8725,7 @@ class ContentSteeringController {
8575
8725
  onSuccess: (response, stats, context, networkDetails) => {
8576
8726
  this.log(`Loaded steering manifest: "${url}"`);
8577
8727
  const steeringData = response.data;
8578
- if (steeringData.VERSION !== 1) {
8728
+ if ((steeringData == null ? void 0 : steeringData.VERSION) !== 1) {
8579
8729
  this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
8580
8730
  return;
8581
8731
  }
@@ -9483,7 +9633,7 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
9483
9633
  });
9484
9634
  function timelineConfig() {
9485
9635
  return {
9486
- cueHandler: Cues,
9636
+ cueHandler: HevcVideoParser,
9487
9637
  // used by timeline-controller
9488
9638
  enableWebVTT: false,
9489
9639
  // used by timeline-controller
@@ -9514,7 +9664,7 @@ function timelineConfig() {
9514
9664
  /**
9515
9665
  * @ignore
9516
9666
  */
9517
- function mergeConfig(defaultConfig, userConfig) {
9667
+ function mergeConfig(defaultConfig, userConfig, logger) {
9518
9668
  if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) {
9519
9669
  throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");
9520
9670
  }
@@ -9584,7 +9734,7 @@ function deepCpy(obj) {
9584
9734
  /**
9585
9735
  * @ignore
9586
9736
  */
9587
- function enableStreamingMode(config) {
9737
+ function enableStreamingMode(config, logger) {
9588
9738
  const currentLoader = config.loader;
9589
9739
  if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) {
9590
9740
  // If a developer has configured their own loader, respect that choice
@@ -9601,10 +9751,9 @@ function enableStreamingMode(config) {
9601
9751
  }
9602
9752
  }
9603
9753
 
9604
- let chromeOrFirefox;
9605
9754
  class LevelController extends BasePlaylistController {
9606
9755
  constructor(hls, contentSteeringController) {
9607
- super(hls, '[level-controller]');
9756
+ super(hls, 'level-controller');
9608
9757
  this._levels = [];
9609
9758
  this._firstLevel = -1;
9610
9759
  this._maxAutoLevel = -1;
@@ -9675,23 +9824,15 @@ class LevelController extends BasePlaylistController {
9675
9824
  let videoCodecFound = false;
9676
9825
  let audioCodecFound = false;
9677
9826
  data.levels.forEach(levelParsed => {
9678
- var _audioCodec, _videoCodec;
9827
+ var _videoCodec;
9679
9828
  const attributes = levelParsed.attrs;
9680
-
9681
- // erase audio codec info if browser does not support mp4a.40.34.
9682
- // demuxer will autodetect codec and fallback to mpeg/audio
9683
9829
  let {
9684
9830
  audioCodec,
9685
9831
  videoCodec
9686
9832
  } = levelParsed;
9687
- if (((_audioCodec = audioCodec) == null ? void 0 : _audioCodec.indexOf('mp4a.40.34')) !== -1) {
9688
- chromeOrFirefox || (chromeOrFirefox = /chrome|firefox/i.test(navigator.userAgent));
9689
- if (chromeOrFirefox) {
9690
- levelParsed.audioCodec = audioCodec = undefined;
9691
- }
9692
- }
9693
9833
  if (audioCodec) {
9694
- levelParsed.audioCodec = audioCodec = getCodecCompatibleName(audioCodec, preferManagedMediaSource);
9834
+ // Returns empty and set to undefined for 'mp4a.40.34' with fallback to 'audio/mpeg' SourceBuffer
9835
+ levelParsed.audioCodec = audioCodec = getCodecCompatibleName(audioCodec, preferManagedMediaSource) || undefined;
9695
9836
  }
9696
9837
  if (((_videoCodec = videoCodec) == null ? void 0 : _videoCodec.indexOf('avc1')) === 0) {
9697
9838
  videoCodec = levelParsed.videoCodec = convertAVC1ToAVCOTI(videoCodec);
@@ -10033,7 +10174,12 @@ class LevelController extends BasePlaylistController {
10033
10174
  if (curLevel.fragmentError === 0) {
10034
10175
  curLevel.loadError = 0;
10035
10176
  }
10036
- this.playlistLoaded(level, data, curLevel.details);
10177
+ // Ignore matching details populated by loading a Media Playlist directly
10178
+ let previousDetails = curLevel.details;
10179
+ if (previousDetails === data.details && previousDetails.advanced) {
10180
+ previousDetails = undefined;
10181
+ }
10182
+ this.playlistLoaded(level, data, previousDetails);
10037
10183
  } else if ((_data$deliveryDirecti2 = data.deliveryDirectives) != null && _data$deliveryDirecti2.skip) {
10038
10184
  // received a delta playlist update that cannot be merged
10039
10185
  details.deltaUpdateFailed = true;
@@ -10211,13 +10357,16 @@ class FragmentTracker {
10211
10357
  * If not found any Fragment, return null
10212
10358
  */
10213
10359
  getBufferedFrag(position, levelType) {
10360
+ return this.getFragAtPos(position, levelType, true);
10361
+ }
10362
+ getFragAtPos(position, levelType, buffered) {
10214
10363
  const {
10215
10364
  fragments
10216
10365
  } = this;
10217
10366
  const keys = Object.keys(fragments);
10218
10367
  for (let i = keys.length; i--;) {
10219
10368
  const fragmentEntity = fragments[keys[i]];
10220
- if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === levelType && fragmentEntity.buffered) {
10369
+ if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === levelType && (!buffered || fragmentEntity.buffered)) {
10221
10370
  const frag = fragmentEntity.body;
10222
10371
  if (frag.start <= position && position <= frag.end) {
10223
10372
  return frag;
@@ -10472,7 +10621,8 @@ class FragmentTracker {
10472
10621
  const {
10473
10622
  frag,
10474
10623
  part,
10475
- timeRanges
10624
+ timeRanges,
10625
+ type
10476
10626
  } = data;
10477
10627
  if (frag.sn === 'initSegment') {
10478
10628
  return;
@@ -10487,10 +10637,8 @@ class FragmentTracker {
10487
10637
  }
10488
10638
  // Store the latest timeRanges loaded in the buffer
10489
10639
  this.timeRanges = timeRanges;
10490
- Object.keys(timeRanges).forEach(elementaryStream => {
10491
- const timeRange = timeRanges[elementaryStream];
10492
- this.detectEvictedFragments(elementaryStream, timeRange, playlistType, part);
10493
- });
10640
+ const timeRange = timeRanges[type];
10641
+ this.detectEvictedFragments(type, timeRange, playlistType, part);
10494
10642
  }
10495
10643
  onFragBuffered(event, data) {
10496
10644
  this.detectPartialFragments(data);
@@ -10819,8 +10967,8 @@ function createLoaderContext(frag, part = null) {
10819
10967
  var _frag$decryptdata;
10820
10968
  let byteRangeStart = start;
10821
10969
  let byteRangeEnd = end;
10822
- if (frag.sn === 'initSegment' && ((_frag$decryptdata = frag.decryptdata) == null ? void 0 : _frag$decryptdata.method) === 'AES-128') {
10823
- // MAP segment encrypted with method 'AES-128', when served with HTTP Range,
10970
+ if (frag.sn === 'initSegment' && isMethodFullSegmentAesCbc((_frag$decryptdata = frag.decryptdata) == null ? void 0 : _frag$decryptdata.method)) {
10971
+ // MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range,
10824
10972
  // has the unencrypted size specified in the range.
10825
10973
  // Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
10826
10974
  const fragmentLen = end - start;
@@ -10853,6 +11001,9 @@ function createGapLoadError(frag, part) {
10853
11001
  (part ? part : frag).stats.aborted = true;
10854
11002
  return new LoadError(errorData);
10855
11003
  }
11004
+ function isMethodFullSegmentAesCbc(method) {
11005
+ return method === 'AES-128' || method === 'AES-256';
11006
+ }
10856
11007
  class LoadError extends Error {
10857
11008
  constructor(data) {
10858
11009
  super(data.error.message);
@@ -10998,6 +11149,8 @@ class KeyLoader {
10998
11149
  }
10999
11150
  return this.loadKeyEME(keyInfo, frag);
11000
11151
  case 'AES-128':
11152
+ case 'AES-256':
11153
+ case 'AES-256-CTR':
11001
11154
  return this.loadKeyHTTP(keyInfo, frag);
11002
11155
  default:
11003
11156
  return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Key supplied with unsupported METHOD: "${decryptdata.method}"`)));
@@ -11133,8 +11286,9 @@ class KeyLoader {
11133
11286
  * we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further
11134
11287
  * task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo).
11135
11288
  */
11136
- class TaskLoop {
11137
- constructor() {
11289
+ class TaskLoop extends Logger {
11290
+ constructor(label, logger) {
11291
+ super(label, logger);
11138
11292
  this._boundTick = void 0;
11139
11293
  this._tickTimer = null;
11140
11294
  this._tickInterval = null;
@@ -11402,33 +11556,61 @@ function alignMediaPlaylistByPDT(details, refDetails) {
11402
11556
  }
11403
11557
 
11404
11558
  class AESCrypto {
11405
- constructor(subtle, iv) {
11559
+ constructor(subtle, iv, aesMode) {
11406
11560
  this.subtle = void 0;
11407
11561
  this.aesIV = void 0;
11562
+ this.aesMode = void 0;
11408
11563
  this.subtle = subtle;
11409
11564
  this.aesIV = iv;
11565
+ this.aesMode = aesMode;
11410
11566
  }
11411
11567
  decrypt(data, key) {
11412
- return this.subtle.decrypt({
11413
- name: 'AES-CBC',
11414
- iv: this.aesIV
11415
- }, key, data);
11568
+ switch (this.aesMode) {
11569
+ case DecrypterAesMode.cbc:
11570
+ return this.subtle.decrypt({
11571
+ name: 'AES-CBC',
11572
+ iv: this.aesIV
11573
+ }, key, data);
11574
+ case DecrypterAesMode.ctr:
11575
+ return this.subtle.decrypt({
11576
+ name: 'AES-CTR',
11577
+ counter: this.aesIV,
11578
+ length: 64
11579
+ },
11580
+ //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block
11581
+ key, data);
11582
+ default:
11583
+ throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`);
11584
+ }
11416
11585
  }
11417
11586
  }
11418
11587
 
11419
11588
  class FastAESKey {
11420
- constructor(subtle, key) {
11589
+ constructor(subtle, key, aesMode) {
11421
11590
  this.subtle = void 0;
11422
11591
  this.key = void 0;
11592
+ this.aesMode = void 0;
11423
11593
  this.subtle = subtle;
11424
11594
  this.key = key;
11595
+ this.aesMode = aesMode;
11425
11596
  }
11426
11597
  expandKey() {
11598
+ const subtleAlgoName = getSubtleAlgoName(this.aesMode);
11427
11599
  return this.subtle.importKey('raw', this.key, {
11428
- name: 'AES-CBC'
11600
+ name: subtleAlgoName
11429
11601
  }, false, ['encrypt', 'decrypt']);
11430
11602
  }
11431
11603
  }
11604
+ function getSubtleAlgoName(aesMode) {
11605
+ switch (aesMode) {
11606
+ case DecrypterAesMode.cbc:
11607
+ return 'AES-CBC';
11608
+ case DecrypterAesMode.ctr:
11609
+ return 'AES-CTR';
11610
+ default:
11611
+ throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`);
11612
+ }
11613
+ }
11432
11614
 
11433
11615
  // PKCS7
11434
11616
  function removePadding(array) {
@@ -11678,7 +11860,8 @@ class Decrypter {
11678
11860
  this.currentIV = null;
11679
11861
  this.currentResult = null;
11680
11862
  this.useSoftware = void 0;
11681
- this.useSoftware = config.enableSoftwareAES;
11863
+ this.enableSoftwareAES = void 0;
11864
+ this.enableSoftwareAES = config.enableSoftwareAES;
11682
11865
  this.removePKCS7Padding = removePKCS7Padding;
11683
11866
  // built in decryptor expects PKCS7 padding
11684
11867
  if (removePKCS7Padding) {
@@ -11691,9 +11874,7 @@ class Decrypter {
11691
11874
  /* no-op */
11692
11875
  }
11693
11876
  }
11694
- if (this.subtle === null) {
11695
- this.useSoftware = true;
11696
- }
11877
+ this.useSoftware = this.subtle === null;
11697
11878
  }
11698
11879
  destroy() {
11699
11880
  this.subtle = null;
@@ -11731,10 +11912,10 @@ class Decrypter {
11731
11912
  this.softwareDecrypter = null;
11732
11913
  }
11733
11914
  }
11734
- decrypt(data, key, iv) {
11915
+ decrypt(data, key, iv, aesMode) {
11735
11916
  if (this.useSoftware) {
11736
11917
  return new Promise((resolve, reject) => {
11737
- this.softwareDecrypt(new Uint8Array(data), key, iv);
11918
+ this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode);
11738
11919
  const decryptResult = this.flush();
11739
11920
  if (decryptResult) {
11740
11921
  resolve(decryptResult.buffer);
@@ -11743,17 +11924,21 @@ class Decrypter {
11743
11924
  }
11744
11925
  });
11745
11926
  }
11746
- return this.webCryptoDecrypt(new Uint8Array(data), key, iv);
11927
+ return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode);
11747
11928
  }
11748
11929
 
11749
11930
  // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
11750
11931
  // data is handled in the flush() call
11751
- softwareDecrypt(data, key, iv) {
11932
+ softwareDecrypt(data, key, iv, aesMode) {
11752
11933
  const {
11753
11934
  currentIV,
11754
11935
  currentResult,
11755
11936
  remainderData
11756
11937
  } = this;
11938
+ if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) {
11939
+ logger.warn('SoftwareDecrypt: can only handle AES-128-CBC');
11940
+ return null;
11941
+ }
11757
11942
  this.logOnce('JS AES decrypt');
11758
11943
  // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
11759
11944
  // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
@@ -11786,11 +11971,11 @@ class Decrypter {
11786
11971
  }
11787
11972
  return result;
11788
11973
  }
11789
- webCryptoDecrypt(data, key, iv) {
11974
+ webCryptoDecrypt(data, key, iv, aesMode) {
11790
11975
  const subtle = this.subtle;
11791
11976
  if (this.key !== key || !this.fastAesKey) {
11792
11977
  this.key = key;
11793
- this.fastAesKey = new FastAESKey(subtle, key);
11978
+ this.fastAesKey = new FastAESKey(subtle, key, aesMode);
11794
11979
  }
11795
11980
  return this.fastAesKey.expandKey().then(aesKey => {
11796
11981
  // decrypt using web crypto
@@ -11798,22 +11983,25 @@ class Decrypter {
11798
11983
  return Promise.reject(new Error('web crypto not initialized'));
11799
11984
  }
11800
11985
  this.logOnce('WebCrypto AES decrypt');
11801
- const crypto = new AESCrypto(subtle, new Uint8Array(iv));
11986
+ const crypto = new AESCrypto(subtle, new Uint8Array(iv), aesMode);
11802
11987
  return crypto.decrypt(data.buffer, aesKey);
11803
11988
  }).catch(err => {
11804
11989
  logger.warn(`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`);
11805
- return this.onWebCryptoError(data, key, iv);
11990
+ return this.onWebCryptoError(data, key, iv, aesMode);
11806
11991
  });
11807
11992
  }
11808
- onWebCryptoError(data, key, iv) {
11809
- this.useSoftware = true;
11810
- this.logEnabled = true;
11811
- this.softwareDecrypt(data, key, iv);
11812
- const decryptResult = this.flush();
11813
- if (decryptResult) {
11814
- return decryptResult.buffer;
11993
+ onWebCryptoError(data, key, iv, aesMode) {
11994
+ const enableSoftwareAES = this.enableSoftwareAES;
11995
+ if (enableSoftwareAES) {
11996
+ this.useSoftware = true;
11997
+ this.logEnabled = true;
11998
+ this.softwareDecrypt(data, key, iv, aesMode);
11999
+ const decryptResult = this.flush();
12000
+ if (decryptResult) {
12001
+ return decryptResult.buffer;
12002
+ }
11815
12003
  }
11816
- throw new Error('WebCrypto and softwareDecrypt: failed to decrypt data');
12004
+ throw new Error('WebCrypto' + (enableSoftwareAES ? ' and softwareDecrypt' : '') + ': failed to decrypt data');
11817
12005
  }
11818
12006
  getValidChunk(data) {
11819
12007
  let currentChunk = data;
@@ -11864,7 +12052,7 @@ const State = {
11864
12052
  };
11865
12053
  class BaseStreamController extends TaskLoop {
11866
12054
  constructor(hls, fragmentTracker, keyLoader, logPrefix, playlistType) {
11867
- super();
12055
+ super(logPrefix, hls.logger);
11868
12056
  this.hls = void 0;
11869
12057
  this.fragPrevious = null;
11870
12058
  this.fragCurrent = null;
@@ -11889,22 +12077,98 @@ class BaseStreamController extends TaskLoop {
11889
12077
  this.startFragRequested = false;
11890
12078
  this.decrypter = void 0;
11891
12079
  this.initPTS = [];
11892
- this.onvseeking = null;
11893
- this.onvended = null;
11894
- this.logPrefix = '';
11895
- this.log = void 0;
11896
- this.warn = void 0;
12080
+ this.buffering = true;
12081
+ this.loadingParts = false;
12082
+ this.onMediaSeeking = () => {
12083
+ const {
12084
+ config,
12085
+ fragCurrent,
12086
+ media,
12087
+ mediaBuffer,
12088
+ state
12089
+ } = this;
12090
+ const currentTime = media ? media.currentTime : 0;
12091
+ const bufferInfo = BufferHelper.bufferInfo(mediaBuffer ? mediaBuffer : media, currentTime, config.maxBufferHole);
12092
+ this.log(`media seeking to ${isFiniteNumber(currentTime) ? currentTime.toFixed(3) : currentTime}, state: ${state}`);
12093
+ if (this.state === State.ENDED) {
12094
+ this.resetLoadingState();
12095
+ } else if (fragCurrent) {
12096
+ // Seeking while frag load is in progress
12097
+ const tolerance = config.maxFragLookUpTolerance;
12098
+ const fragStartOffset = fragCurrent.start - tolerance;
12099
+ const fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance;
12100
+ // if seeking out of buffered range or into new one
12101
+ if (!bufferInfo.len || fragEndOffset < bufferInfo.start || fragStartOffset > bufferInfo.end) {
12102
+ const pastFragment = currentTime > fragEndOffset;
12103
+ // if the seek position is outside the current fragment range
12104
+ if (currentTime < fragStartOffset || pastFragment) {
12105
+ if (pastFragment && fragCurrent.loader) {
12106
+ this.log('seeking outside of buffer while fragment load in progress, cancel fragment load');
12107
+ fragCurrent.abortRequests();
12108
+ this.resetLoadingState();
12109
+ }
12110
+ this.fragPrevious = null;
12111
+ }
12112
+ }
12113
+ }
12114
+ if (media) {
12115
+ // Remove gap fragments
12116
+ this.fragmentTracker.removeFragmentsInRange(currentTime, Infinity, this.playlistType, true);
12117
+ this.lastCurrentTime = currentTime;
12118
+ if (!this.loadingParts) {
12119
+ const bufferEnd = Math.max(bufferInfo.end, currentTime);
12120
+ const shouldLoadParts = this.shouldLoadParts(this.getLevelDetails(), bufferEnd);
12121
+ if (shouldLoadParts) {
12122
+ this.log(`LL-Part loading ON after seeking to ${currentTime.toFixed(2)} with buffer @${bufferEnd.toFixed(2)}`);
12123
+ this.loadingParts = shouldLoadParts;
12124
+ }
12125
+ }
12126
+ }
12127
+
12128
+ // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
12129
+ if (!this.loadedmetadata && !bufferInfo.len) {
12130
+ this.nextLoadPosition = this.startPosition = currentTime;
12131
+ }
12132
+
12133
+ // Async tick to speed up processing
12134
+ this.tickImmediate();
12135
+ };
12136
+ this.onMediaEnded = () => {
12137
+ // reset startPosition and lastCurrentTime to restart playback @ stream beginning
12138
+ this.startPosition = this.lastCurrentTime = 0;
12139
+ if (this.playlistType === PlaylistLevelType.MAIN) {
12140
+ this.hls.trigger(Events.MEDIA_ENDED, {
12141
+ stalled: false
12142
+ });
12143
+ }
12144
+ };
11897
12145
  this.playlistType = playlistType;
11898
- this.logPrefix = logPrefix;
11899
- this.log = logger.log.bind(logger, `${logPrefix}:`);
11900
- this.warn = logger.warn.bind(logger, `${logPrefix}:`);
11901
12146
  this.hls = hls;
11902
12147
  this.fragmentLoader = new FragmentLoader(hls.config);
11903
12148
  this.keyLoader = keyLoader;
11904
12149
  this.fragmentTracker = fragmentTracker;
11905
12150
  this.config = hls.config;
11906
12151
  this.decrypter = new Decrypter(hls.config);
12152
+ }
12153
+ registerListeners() {
12154
+ const {
12155
+ hls
12156
+ } = this;
12157
+ hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
12158
+ hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
12159
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
11907
12160
  hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
12161
+ hls.on(Events.ERROR, this.onError, this);
12162
+ }
12163
+ unregisterListeners() {
12164
+ const {
12165
+ hls
12166
+ } = this;
12167
+ hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
12168
+ hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
12169
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
12170
+ hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
12171
+ hls.off(Events.ERROR, this.onError, this);
11908
12172
  }
11909
12173
  doTick() {
11910
12174
  this.onTickEnd();
@@ -11928,6 +12192,12 @@ class BaseStreamController extends TaskLoop {
11928
12192
  this.clearNextTick();
11929
12193
  this.state = State.STOPPED;
11930
12194
  }
12195
+ pauseBuffering() {
12196
+ this.buffering = false;
12197
+ }
12198
+ resumeBuffering() {
12199
+ this.buffering = true;
12200
+ }
11931
12201
  _streamEnded(bufferInfo, levelDetails) {
11932
12202
  // If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached,
11933
12203
  // of nothing loading/loaded return false
@@ -11958,10 +12228,8 @@ class BaseStreamController extends TaskLoop {
11958
12228
  }
11959
12229
  onMediaAttached(event, data) {
11960
12230
  const media = this.media = this.mediaBuffer = data.media;
11961
- this.onvseeking = this.onMediaSeeking.bind(this);
11962
- this.onvended = this.onMediaEnded.bind(this);
11963
- media.addEventListener('seeking', this.onvseeking);
11964
- media.addEventListener('ended', this.onvended);
12231
+ media.addEventListener('seeking', this.onMediaSeeking);
12232
+ media.addEventListener('ended', this.onMediaEnded);
11965
12233
  const config = this.config;
11966
12234
  if (this.levels && config.autoStartLoad && this.state === State.STOPPED) {
11967
12235
  this.startLoad(config.startPosition);
@@ -11975,10 +12243,9 @@ class BaseStreamController extends TaskLoop {
11975
12243
  }
11976
12244
 
11977
12245
  // remove video listeners
11978
- if (media && this.onvseeking && this.onvended) {
11979
- media.removeEventListener('seeking', this.onvseeking);
11980
- media.removeEventListener('ended', this.onvended);
11981
- this.onvseeking = this.onvended = null;
12246
+ if (media) {
12247
+ media.removeEventListener('seeking', this.onMediaSeeking);
12248
+ media.removeEventListener('ended', this.onMediaEnded);
11982
12249
  }
11983
12250
  if (this.keyLoader) {
11984
12251
  this.keyLoader.detach();
@@ -11988,56 +12255,8 @@ class BaseStreamController extends TaskLoop {
11988
12255
  this.fragmentTracker.removeAllFragments();
11989
12256
  this.stopLoad();
11990
12257
  }
11991
- onMediaSeeking() {
11992
- const {
11993
- config,
11994
- fragCurrent,
11995
- media,
11996
- mediaBuffer,
11997
- state
11998
- } = this;
11999
- const currentTime = media ? media.currentTime : 0;
12000
- const bufferInfo = BufferHelper.bufferInfo(mediaBuffer ? mediaBuffer : media, currentTime, config.maxBufferHole);
12001
- this.log(`media seeking to ${isFiniteNumber(currentTime) ? currentTime.toFixed(3) : currentTime}, state: ${state}`);
12002
- if (this.state === State.ENDED) {
12003
- this.resetLoadingState();
12004
- } else if (fragCurrent) {
12005
- // Seeking while frag load is in progress
12006
- const tolerance = config.maxFragLookUpTolerance;
12007
- const fragStartOffset = fragCurrent.start - tolerance;
12008
- const fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance;
12009
- // if seeking out of buffered range or into new one
12010
- if (!bufferInfo.len || fragEndOffset < bufferInfo.start || fragStartOffset > bufferInfo.end) {
12011
- const pastFragment = currentTime > fragEndOffset;
12012
- // if the seek position is outside the current fragment range
12013
- if (currentTime < fragStartOffset || pastFragment) {
12014
- if (pastFragment && fragCurrent.loader) {
12015
- this.log('seeking outside of buffer while fragment load in progress, cancel fragment load');
12016
- fragCurrent.abortRequests();
12017
- this.resetLoadingState();
12018
- }
12019
- this.fragPrevious = null;
12020
- }
12021
- }
12022
- }
12023
- if (media) {
12024
- // Remove gap fragments
12025
- this.fragmentTracker.removeFragmentsInRange(currentTime, Infinity, this.playlistType, true);
12026
- this.lastCurrentTime = currentTime;
12027
- }
12028
-
12029
- // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
12030
- if (!this.loadedmetadata && !bufferInfo.len) {
12031
- this.nextLoadPosition = this.startPosition = currentTime;
12032
- }
12033
-
12034
- // Async tick to speed up processing
12035
- this.tickImmediate();
12036
- }
12037
- onMediaEnded() {
12038
- // reset startPosition and lastCurrentTime to restart playback @ stream beginning
12039
- this.startPosition = this.lastCurrentTime = 0;
12040
- }
12258
+ onManifestLoading() {}
12259
+ onError(event, data) {}
12041
12260
  onManifestLoaded(event, data) {
12042
12261
  this.startTimeOffset = data.startTimeOffset;
12043
12262
  this.initPTS = [];
@@ -12047,7 +12266,7 @@ class BaseStreamController extends TaskLoop {
12047
12266
  this.stopLoad();
12048
12267
  super.onHandlerDestroying();
12049
12268
  // @ts-ignore
12050
- this.hls = null;
12269
+ this.hls = this.onMediaSeeking = this.onMediaEnded = null;
12051
12270
  }
12052
12271
  onHandlerDestroyed() {
12053
12272
  this.state = State.STOPPED;
@@ -12178,10 +12397,10 @@ class BaseStreamController extends TaskLoop {
12178
12397
  const decryptData = frag.decryptdata;
12179
12398
 
12180
12399
  // check to see if the payload needs to be decrypted
12181
- if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && decryptData.method === 'AES-128') {
12400
+ if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && isFullSegmentEncryption(decryptData.method)) {
12182
12401
  const startTime = self.performance.now();
12183
12402
  // decrypt init segment data
12184
- return this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer).catch(err => {
12403
+ return this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, getAesModeFromFullSegmentMethod(decryptData.method)).catch(err => {
12185
12404
  hls.trigger(Events.ERROR, {
12186
12405
  type: ErrorTypes.MEDIA_ERROR,
12187
12406
  details: ErrorDetails.FRAG_DECRYPT_ERROR,
@@ -12293,7 +12512,7 @@ class BaseStreamController extends TaskLoop {
12293
12512
  }
12294
12513
  let keyLoadingPromise = null;
12295
12514
  if (frag.encrypted && !((_frag$decryptdata = frag.decryptdata) != null && _frag$decryptdata.key)) {
12296
- this.log(`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'} ${frag.level}`);
12515
+ this.log(`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'} ${frag.level}`);
12297
12516
  this.state = State.KEY_LOADING;
12298
12517
  this.fragCurrent = frag;
12299
12518
  keyLoadingPromise = this.keyLoader.load(frag).then(keyLoadedData => {
@@ -12314,8 +12533,16 @@ class BaseStreamController extends TaskLoop {
12314
12533
  } else if (!frag.encrypted && details.encryptedFragments.length) {
12315
12534
  this.keyLoader.loadClear(frag, details.encryptedFragments);
12316
12535
  }
12536
+ const fragPrevious = this.fragPrevious;
12537
+ if (frag.sn !== 'initSegment' && (!fragPrevious || frag.sn !== fragPrevious.sn)) {
12538
+ const shouldLoadParts = this.shouldLoadParts(level.details, frag.end);
12539
+ if (shouldLoadParts !== this.loadingParts) {
12540
+ this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} loading sn ${fragPrevious == null ? void 0 : fragPrevious.sn}->${frag.sn}`);
12541
+ this.loadingParts = shouldLoadParts;
12542
+ }
12543
+ }
12317
12544
  targetBufferTime = Math.max(frag.start, targetBufferTime || 0);
12318
- if (this.config.lowLatencyMode && frag.sn !== 'initSegment') {
12545
+ if (this.loadingParts && frag.sn !== 'initSegment') {
12319
12546
  const partList = details.partList;
12320
12547
  if (partList && progressCallback) {
12321
12548
  if (targetBufferTime > frag.end && details.fragmentHint) {
@@ -12324,7 +12551,7 @@ class BaseStreamController extends TaskLoop {
12324
12551
  const partIndex = this.getNextPart(partList, frag, targetBufferTime);
12325
12552
  if (partIndex > -1) {
12326
12553
  const part = partList[partIndex];
12327
- this.log(`Loading part sn: ${frag.sn} p: ${part.index} cc: ${frag.cc} of playlist [${details.startSN}-${details.endSN}] parts [0-${partIndex}-${partList.length - 1}] ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`);
12554
+ this.log(`Loading part sn: ${frag.sn} p: ${part.index} cc: ${frag.cc} of playlist [${details.startSN}-${details.endSN}] parts [0-${partIndex}-${partList.length - 1}] ${this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`);
12328
12555
  this.nextLoadPosition = part.start + part.duration;
12329
12556
  this.state = State.FRAG_LOADING;
12330
12557
  let _result;
@@ -12353,7 +12580,14 @@ class BaseStreamController extends TaskLoop {
12353
12580
  }
12354
12581
  }
12355
12582
  }
12356
- this.log(`Loading fragment ${frag.sn} cc: ${frag.cc} ${details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''}${this.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`);
12583
+ if (frag.sn !== 'initSegment' && this.loadingParts) {
12584
+ this.log(`LL-Part loading OFF after next part miss @${targetBufferTime.toFixed(2)}`);
12585
+ this.loadingParts = false;
12586
+ } else if (!frag.url) {
12587
+ // Selected fragment hint for part but not loading parts
12588
+ return Promise.resolve(null);
12589
+ }
12590
+ this.log(`Loading fragment ${frag.sn} cc: ${frag.cc} ${details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''}${this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`);
12357
12591
  // Don't update nextLoadPosition for fragments which are not buffered
12358
12592
  if (isFiniteNumber(frag.sn) && !this.bitrateTest) {
12359
12593
  this.nextLoadPosition = frag.start + frag.duration;
@@ -12451,8 +12685,36 @@ class BaseStreamController extends TaskLoop {
12451
12685
  if (part) {
12452
12686
  part.stats.parsing.end = now;
12453
12687
  }
12688
+ // See if part loading should be disabled/enabled based on buffer and playback position.
12689
+ if (frag.sn !== 'initSegment') {
12690
+ const levelDetails = this.getLevelDetails();
12691
+ const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
12692
+ const shouldLoadParts = loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
12693
+ if (shouldLoadParts !== this.loadingParts) {
12694
+ this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} after parsing segment ending @${frag.end.toFixed(2)}`);
12695
+ this.loadingParts = shouldLoadParts;
12696
+ }
12697
+ }
12454
12698
  this.updateLevelTiming(frag, part, level, chunkMeta.partial);
12455
12699
  }
12700
+ shouldLoadParts(details, bufferEnd) {
12701
+ if (this.config.lowLatencyMode) {
12702
+ if (!details) {
12703
+ return this.loadingParts;
12704
+ }
12705
+ if (details != null && details.partList) {
12706
+ var _details$fragmentHint;
12707
+ // Buffer must be ahead of first part + duration of parts after last segment
12708
+ // and playback must be at or past segment adjacent to part list
12709
+ const firstPart = details.partList[0];
12710
+ const safePartStart = firstPart.end + (((_details$fragmentHint = details.fragmentHint) == null ? void 0 : _details$fragmentHint.duration) || 0);
12711
+ if (bufferEnd >= safePartStart && this.lastCurrentTime > firstPart.start - firstPart.fragment.duration) {
12712
+ return true;
12713
+ }
12714
+ }
12715
+ }
12716
+ return false;
12717
+ }
12456
12718
  getCurrentContext(chunkMeta) {
12457
12719
  const {
12458
12720
  levels,
@@ -12553,7 +12815,7 @@ class BaseStreamController extends TaskLoop {
12553
12815
  // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos
12554
12816
  if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
12555
12817
  const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
12556
- if (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) {
12818
+ if (bufferedFragAtPos && (bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)) {
12557
12819
  return BufferHelper.bufferInfo(bufferable, pos, Math.max(bufferInfo.nextStart, maxBufferHole));
12558
12820
  }
12559
12821
  }
@@ -12601,7 +12863,8 @@ class BaseStreamController extends TaskLoop {
12601
12863
  config
12602
12864
  } = this;
12603
12865
  const start = fragments[0].start;
12604
- let frag;
12866
+ const canLoadParts = config.lowLatencyMode && !!levelDetails.partList;
12867
+ let frag = null;
12605
12868
  if (levelDetails.live) {
12606
12869
  const initialLiveManifestSize = config.initialLiveManifestSize;
12607
12870
  if (fragLen < initialLiveManifestSize) {
@@ -12613,6 +12876,10 @@ class BaseStreamController extends TaskLoop {
12613
12876
  // Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that
12614
12877
  // we get the fragment matching that start time
12615
12878
  if (!levelDetails.PTSKnown && !this.startFragRequested && this.startPosition === -1 || pos < start) {
12879
+ if (canLoadParts && !this.loadingParts) {
12880
+ this.log(`LL-Part loading ON for initial live fragment`);
12881
+ this.loadingParts = true;
12882
+ }
12616
12883
  frag = this.getInitialLiveFragment(levelDetails, fragments);
12617
12884
  this.startPosition = this.nextLoadPosition = frag ? this.hls.liveSyncPosition || frag.start : pos;
12618
12885
  }
@@ -12623,7 +12890,7 @@ class BaseStreamController extends TaskLoop {
12623
12890
 
12624
12891
  // If we haven't run into any special cases already, just load the fragment most closely matching the requested position
12625
12892
  if (!frag) {
12626
- const end = config.lowLatencyMode ? levelDetails.partEnd : levelDetails.fragmentEnd;
12893
+ const end = this.loadingParts ? levelDetails.partEnd : levelDetails.fragmentEnd;
12627
12894
  frag = this.getFragmentAtPosition(pos, end, levelDetails);
12628
12895
  }
12629
12896
  return this.mapToInitFragWhenRequired(frag);
@@ -12745,7 +13012,7 @@ class BaseStreamController extends TaskLoop {
12745
13012
  } = levelDetails;
12746
13013
  const tolerance = config.maxFragLookUpTolerance;
12747
13014
  const partList = levelDetails.partList;
12748
- const loadingParts = !!(config.lowLatencyMode && partList != null && partList.length && fragmentHint);
13015
+ const loadingParts = !!(this.loadingParts && partList != null && partList.length && fragmentHint);
12749
13016
  if (loadingParts && fragmentHint && !this.bitrateTest) {
12750
13017
  // Include incomplete fragment with parts at end
12751
13018
  fragments = fragments.concat(fragmentHint);
@@ -12938,7 +13205,7 @@ class BaseStreamController extends TaskLoop {
12938
13205
  errorAction.resolved = true;
12939
13206
  }
12940
13207
  } else {
12941
- logger.warn(`${data.details} reached or exceeded max retry (${retryCount})`);
13208
+ this.warn(`${data.details} reached or exceeded max retry (${retryCount})`);
12942
13209
  return;
12943
13210
  }
12944
13211
  } else if ((errorAction == null ? void 0 : errorAction.action) === NetworkErrorAction.SendAlternateToPenaltyBox) {
@@ -13006,7 +13273,9 @@ class BaseStreamController extends TaskLoop {
13006
13273
  this.log('Reset loading state');
13007
13274
  this.fragCurrent = null;
13008
13275
  this.fragPrevious = null;
13009
- this.state = State.IDLE;
13276
+ if (this.state !== State.STOPPED) {
13277
+ this.state = State.IDLE;
13278
+ }
13010
13279
  }
13011
13280
  resetStartWhenNotLoaded(level) {
13012
13281
  // if loadedmetadata is not set, it means that first frag request failed
@@ -13333,6 +13602,7 @@ const initPTSFn = (timestamp, timeOffset, initPTS) => {
13333
13602
  */
13334
13603
  function getAudioConfig(observer, data, offset, audioCodec) {
13335
13604
  let adtsObjectType;
13605
+ let originalAdtsObjectType;
13336
13606
  let adtsExtensionSamplingIndex;
13337
13607
  let adtsChannelConfig;
13338
13608
  let config;
@@ -13340,7 +13610,7 @@ function getAudioConfig(observer, data, offset, audioCodec) {
13340
13610
  const manifestCodec = audioCodec;
13341
13611
  const adtsSamplingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
13342
13612
  // byte 2
13343
- adtsObjectType = ((data[offset + 2] & 0xc0) >>> 6) + 1;
13613
+ adtsObjectType = originalAdtsObjectType = ((data[offset + 2] & 0xc0) >>> 6) + 1;
13344
13614
  const adtsSamplingIndex = (data[offset + 2] & 0x3c) >>> 2;
13345
13615
  if (adtsSamplingIndex > adtsSamplingRates.length - 1) {
13346
13616
  const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`);
@@ -13357,8 +13627,8 @@ function getAudioConfig(observer, data, offset, audioCodec) {
13357
13627
  // byte 3
13358
13628
  adtsChannelConfig |= (data[offset + 3] & 0xc0) >>> 6;
13359
13629
  logger.log(`manifest codec:${audioCodec}, ADTS type:${adtsObjectType}, samplingIndex:${adtsSamplingIndex}`);
13360
- // firefox: freq less than 24kHz = AAC SBR (HE-AAC)
13361
- if (/firefox/i.test(userAgent)) {
13630
+ // Firefox and Pale Moon: freq less than 24kHz = AAC SBR (HE-AAC)
13631
+ if (/firefox|palemoon/i.test(userAgent)) {
13362
13632
  if (adtsSamplingIndex >= 6) {
13363
13633
  adtsObjectType = 5;
13364
13634
  config = new Array(4);
@@ -13452,6 +13722,7 @@ function getAudioConfig(observer, data, offset, audioCodec) {
13452
13722
  samplerate: adtsSamplingRates[adtsSamplingIndex],
13453
13723
  channelCount: adtsChannelConfig,
13454
13724
  codec: 'mp4a.40.' + adtsObjectType,
13725
+ parsedCodec: 'mp4a.40.' + originalAdtsObjectType,
13455
13726
  manifestCodec
13456
13727
  };
13457
13728
  }
@@ -13506,7 +13777,8 @@ function initTrackConfig(track, observer, data, offset, audioCodec) {
13506
13777
  track.channelCount = config.channelCount;
13507
13778
  track.codec = config.codec;
13508
13779
  track.manifestCodec = config.manifestCodec;
13509
- logger.log(`parsed codec:${track.codec}, rate:${config.samplerate}, channels:${config.channelCount}`);
13780
+ track.parsedCodec = config.parsedCodec;
13781
+ logger.log(`parsed codec:${track.parsedCodec}, codec:${track.codec}, rate:${config.samplerate}, channels:${config.channelCount}`);
13510
13782
  }
13511
13783
  }
13512
13784
  function getFrameDuration(samplerate) {
@@ -13984,6 +14256,110 @@ class BaseVideoParser {
13984
14256
  logger.log(VideoSample.pts + '/' + VideoSample.dts + ':' + VideoSample.debug);
13985
14257
  }
13986
14258
  }
14259
+ parseNALu(track, array) {
14260
+ const len = array.byteLength;
14261
+ let state = track.naluState || 0;
14262
+ const lastState = state;
14263
+ const units = [];
14264
+ let i = 0;
14265
+ let value;
14266
+ let overflow;
14267
+ let unitType;
14268
+ let lastUnitStart = -1;
14269
+ let lastUnitType = 0;
14270
+ // logger.log('PES:' + Hex.hexDump(array));
14271
+
14272
+ if (state === -1) {
14273
+ // special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet
14274
+ lastUnitStart = 0;
14275
+ // NALu type is value read from offset 0
14276
+ lastUnitType = this.getNALuType(array, 0);
14277
+ state = 0;
14278
+ i = 1;
14279
+ }
14280
+ while (i < len) {
14281
+ value = array[i++];
14282
+ // optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case
14283
+ if (!state) {
14284
+ state = value ? 0 : 1;
14285
+ continue;
14286
+ }
14287
+ if (state === 1) {
14288
+ state = value ? 0 : 2;
14289
+ continue;
14290
+ }
14291
+ // here we have state either equal to 2 or 3
14292
+ if (!value) {
14293
+ state = 3;
14294
+ } else if (value === 1) {
14295
+ overflow = i - state - 1;
14296
+ if (lastUnitStart >= 0) {
14297
+ const unit = {
14298
+ data: array.subarray(lastUnitStart, overflow),
14299
+ type: lastUnitType
14300
+ };
14301
+ // logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
14302
+ units.push(unit);
14303
+ } else {
14304
+ // lastUnitStart is undefined => this is the first start code found in this PES packet
14305
+ // first check if start code delimiter is overlapping between 2 PES packets,
14306
+ // ie it started in last packet (lastState not zero)
14307
+ // and ended at the beginning of this PES packet (i <= 4 - lastState)
14308
+ const lastUnit = this.getLastNalUnit(track.samples);
14309
+ if (lastUnit) {
14310
+ if (lastState && i <= 4 - lastState) {
14311
+ // start delimiter overlapping between PES packets
14312
+ // strip start delimiter bytes from the end of last NAL unit
14313
+ // check if lastUnit had a state different from zero
14314
+ if (lastUnit.state) {
14315
+ // strip last bytes
14316
+ lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState);
14317
+ }
14318
+ }
14319
+ // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
14320
+
14321
+ if (overflow > 0) {
14322
+ // logger.log('first NALU found with overflow:' + overflow);
14323
+ lastUnit.data = appendUint8Array(lastUnit.data, array.subarray(0, overflow));
14324
+ lastUnit.state = 0;
14325
+ }
14326
+ }
14327
+ }
14328
+ // check if we can read unit type
14329
+ if (i < len) {
14330
+ unitType = this.getNALuType(array, i);
14331
+ // logger.log('find NALU @ offset:' + i + ',type:' + unitType);
14332
+ lastUnitStart = i;
14333
+ lastUnitType = unitType;
14334
+ state = 0;
14335
+ } else {
14336
+ // not enough byte to read unit type. let's read it on next PES parsing
14337
+ state = -1;
14338
+ }
14339
+ } else {
14340
+ state = 0;
14341
+ }
14342
+ }
14343
+ if (lastUnitStart >= 0 && state >= 0) {
14344
+ const unit = {
14345
+ data: array.subarray(lastUnitStart, len),
14346
+ type: lastUnitType,
14347
+ state: state
14348
+ };
14349
+ units.push(unit);
14350
+ // logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state);
14351
+ }
14352
+ // no NALu found
14353
+ if (units.length === 0) {
14354
+ // append pes.data to previous NAL unit
14355
+ const lastUnit = this.getLastNalUnit(track.samples);
14356
+ if (lastUnit) {
14357
+ lastUnit.data = appendUint8Array(lastUnit.data, array);
14358
+ }
14359
+ }
14360
+ track.naluState = state;
14361
+ return units;
14362
+ }
13987
14363
  }
13988
14364
 
13989
14365
  /**
@@ -14126,194 +14502,11 @@ class ExpGolomb {
14126
14502
  readUInt() {
14127
14503
  return this.readBits(32);
14128
14504
  }
14129
-
14130
- /**
14131
- * Advance the ExpGolomb decoder past a scaling list. The scaling
14132
- * list is optionally transmitted as part of a sequence parameter
14133
- * set and is not relevant to transmuxing.
14134
- * @param count the number of entries in this scaling list
14135
- * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
14136
- */
14137
- skipScalingList(count) {
14138
- let lastScale = 8;
14139
- let nextScale = 8;
14140
- let deltaScale;
14141
- for (let j = 0; j < count; j++) {
14142
- if (nextScale !== 0) {
14143
- deltaScale = this.readEG();
14144
- nextScale = (lastScale + deltaScale + 256) % 256;
14145
- }
14146
- lastScale = nextScale === 0 ? lastScale : nextScale;
14147
- }
14148
- }
14149
-
14150
- /**
14151
- * Read a sequence parameter set and return some interesting video
14152
- * properties. A sequence parameter set is the H264 metadata that
14153
- * describes the properties of upcoming video frames.
14154
- * @returns an object with configuration parsed from the
14155
- * sequence parameter set, including the dimensions of the
14156
- * associated video frames.
14157
- */
14158
- readSPS() {
14159
- let frameCropLeftOffset = 0;
14160
- let frameCropRightOffset = 0;
14161
- let frameCropTopOffset = 0;
14162
- let frameCropBottomOffset = 0;
14163
- let numRefFramesInPicOrderCntCycle;
14164
- let scalingListCount;
14165
- let i;
14166
- const readUByte = this.readUByte.bind(this);
14167
- const readBits = this.readBits.bind(this);
14168
- const readUEG = this.readUEG.bind(this);
14169
- const readBoolean = this.readBoolean.bind(this);
14170
- const skipBits = this.skipBits.bind(this);
14171
- const skipEG = this.skipEG.bind(this);
14172
- const skipUEG = this.skipUEG.bind(this);
14173
- const skipScalingList = this.skipScalingList.bind(this);
14174
- readUByte();
14175
- const profileIdc = readUByte(); // profile_idc
14176
- readBits(5); // profileCompat constraint_set[0-4]_flag, u(5)
14177
- skipBits(3); // reserved_zero_3bits u(3),
14178
- readUByte(); // level_idc u(8)
14179
- skipUEG(); // seq_parameter_set_id
14180
- // some profiles have more optional data we don't need
14181
- if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) {
14182
- const chromaFormatIdc = readUEG();
14183
- if (chromaFormatIdc === 3) {
14184
- skipBits(1);
14185
- } // separate_colour_plane_flag
14186
-
14187
- skipUEG(); // bit_depth_luma_minus8
14188
- skipUEG(); // bit_depth_chroma_minus8
14189
- skipBits(1); // qpprime_y_zero_transform_bypass_flag
14190
- if (readBoolean()) {
14191
- // seq_scaling_matrix_present_flag
14192
- scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
14193
- for (i = 0; i < scalingListCount; i++) {
14194
- if (readBoolean()) {
14195
- // seq_scaling_list_present_flag[ i ]
14196
- if (i < 6) {
14197
- skipScalingList(16);
14198
- } else {
14199
- skipScalingList(64);
14200
- }
14201
- }
14202
- }
14203
- }
14204
- }
14205
- skipUEG(); // log2_max_frame_num_minus4
14206
- const picOrderCntType = readUEG();
14207
- if (picOrderCntType === 0) {
14208
- readUEG(); // log2_max_pic_order_cnt_lsb_minus4
14209
- } else if (picOrderCntType === 1) {
14210
- skipBits(1); // delta_pic_order_always_zero_flag
14211
- skipEG(); // offset_for_non_ref_pic
14212
- skipEG(); // offset_for_top_to_bottom_field
14213
- numRefFramesInPicOrderCntCycle = readUEG();
14214
- for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
14215
- skipEG();
14216
- } // offset_for_ref_frame[ i ]
14217
- }
14218
- skipUEG(); // max_num_ref_frames
14219
- skipBits(1); // gaps_in_frame_num_value_allowed_flag
14220
- const picWidthInMbsMinus1 = readUEG();
14221
- const picHeightInMapUnitsMinus1 = readUEG();
14222
- const frameMbsOnlyFlag = readBits(1);
14223
- if (frameMbsOnlyFlag === 0) {
14224
- skipBits(1);
14225
- } // mb_adaptive_frame_field_flag
14226
-
14227
- skipBits(1); // direct_8x8_inference_flag
14228
- if (readBoolean()) {
14229
- // frame_cropping_flag
14230
- frameCropLeftOffset = readUEG();
14231
- frameCropRightOffset = readUEG();
14232
- frameCropTopOffset = readUEG();
14233
- frameCropBottomOffset = readUEG();
14234
- }
14235
- let pixelRatio = [1, 1];
14236
- if (readBoolean()) {
14237
- // vui_parameters_present_flag
14238
- if (readBoolean()) {
14239
- // aspect_ratio_info_present_flag
14240
- const aspectRatioIdc = readUByte();
14241
- switch (aspectRatioIdc) {
14242
- case 1:
14243
- pixelRatio = [1, 1];
14244
- break;
14245
- case 2:
14246
- pixelRatio = [12, 11];
14247
- break;
14248
- case 3:
14249
- pixelRatio = [10, 11];
14250
- break;
14251
- case 4:
14252
- pixelRatio = [16, 11];
14253
- break;
14254
- case 5:
14255
- pixelRatio = [40, 33];
14256
- break;
14257
- case 6:
14258
- pixelRatio = [24, 11];
14259
- break;
14260
- case 7:
14261
- pixelRatio = [20, 11];
14262
- break;
14263
- case 8:
14264
- pixelRatio = [32, 11];
14265
- break;
14266
- case 9:
14267
- pixelRatio = [80, 33];
14268
- break;
14269
- case 10:
14270
- pixelRatio = [18, 11];
14271
- break;
14272
- case 11:
14273
- pixelRatio = [15, 11];
14274
- break;
14275
- case 12:
14276
- pixelRatio = [64, 33];
14277
- break;
14278
- case 13:
14279
- pixelRatio = [160, 99];
14280
- break;
14281
- case 14:
14282
- pixelRatio = [4, 3];
14283
- break;
14284
- case 15:
14285
- pixelRatio = [3, 2];
14286
- break;
14287
- case 16:
14288
- pixelRatio = [2, 1];
14289
- break;
14290
- case 255:
14291
- {
14292
- pixelRatio = [readUByte() << 8 | readUByte(), readUByte() << 8 | readUByte()];
14293
- break;
14294
- }
14295
- }
14296
- }
14297
- }
14298
- return {
14299
- width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2),
14300
- height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset),
14301
- pixelRatio: pixelRatio
14302
- };
14303
- }
14304
- readSliceType() {
14305
- // skip NALu type
14306
- this.readUByte();
14307
- // discard first_mb_in_slice
14308
- this.readUEG();
14309
- // return slice_type
14310
- return this.readUEG();
14311
- }
14312
14505
  }
14313
14506
 
14314
14507
  class AvcVideoParser extends BaseVideoParser {
14315
- parseAVCPES(track, textTrack, pes, last, duration) {
14316
- const units = this.parseAVCNALu(track, pes.data);
14508
+ parsePES(track, textTrack, pes, last, duration) {
14509
+ const units = this.parseNALu(track, pes.data);
14317
14510
  let VideoSample = this.VideoSample;
14318
14511
  let push;
14319
14512
  let spsfound = false;
@@ -14338,7 +14531,7 @@ class AvcVideoParser extends BaseVideoParser {
14338
14531
  // only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...)
14339
14532
  if (spsfound && data.length > 4) {
14340
14533
  // retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR
14341
- const sliceType = new ExpGolomb(data).readSliceType();
14534
+ const sliceType = this.readSliceType(data);
14342
14535
  // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice
14343
14536
  // SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples.
14344
14537
  // An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice.
@@ -14392,8 +14585,7 @@ class AvcVideoParser extends BaseVideoParser {
14392
14585
  push = true;
14393
14586
  spsfound = true;
14394
14587
  const sps = unit.data;
14395
- const expGolombDecoder = new ExpGolomb(sps);
14396
- const config = expGolombDecoder.readSPS();
14588
+ const config = this.readSPS(sps);
14397
14589
  if (!track.sps || track.width !== config.width || track.height !== config.height || ((_track$pixelRatio = track.pixelRatio) == null ? void 0 : _track$pixelRatio[0]) !== config.pixelRatio[0] || ((_track$pixelRatio2 = track.pixelRatio) == null ? void 0 : _track$pixelRatio2[1]) !== config.pixelRatio[1]) {
14398
14590
  track.width = config.width;
14399
14591
  track.height = config.height;
@@ -14449,109 +14641,192 @@ class AvcVideoParser extends BaseVideoParser {
14449
14641
  this.VideoSample = null;
14450
14642
  }
14451
14643
  }
14452
- parseAVCNALu(track, array) {
14453
- const len = array.byteLength;
14454
- let state = track.naluState || 0;
14455
- const lastState = state;
14456
- const units = [];
14457
- let i = 0;
14458
- let value;
14459
- let overflow;
14460
- let unitType;
14461
- let lastUnitStart = -1;
14462
- let lastUnitType = 0;
14463
- // logger.log('PES:' + Hex.hexDump(array));
14644
+ getNALuType(data, offset) {
14645
+ return data[offset] & 0x1f;
14646
+ }
14647
+ readSliceType(data) {
14648
+ const eg = new ExpGolomb(data);
14649
+ // skip NALu type
14650
+ eg.readUByte();
14651
+ // discard first_mb_in_slice
14652
+ eg.readUEG();
14653
+ // return slice_type
14654
+ return eg.readUEG();
14655
+ }
14464
14656
 
14465
- if (state === -1) {
14466
- // special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet
14467
- lastUnitStart = 0;
14468
- // NALu type is value read from offset 0
14469
- lastUnitType = array[0] & 0x1f;
14470
- state = 0;
14471
- i = 1;
14472
- }
14473
- while (i < len) {
14474
- value = array[i++];
14475
- // optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case
14476
- if (!state) {
14477
- state = value ? 0 : 1;
14478
- continue;
14479
- }
14480
- if (state === 1) {
14481
- state = value ? 0 : 2;
14482
- continue;
14657
+ /**
14658
+ * The scaling list is optionally transmitted as part of a sequence parameter
14659
+ * set and is not relevant to transmuxing.
14660
+ * @param count the number of entries in this scaling list
14661
+ * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
14662
+ */
14663
+ skipScalingList(count, reader) {
14664
+ let lastScale = 8;
14665
+ let nextScale = 8;
14666
+ let deltaScale;
14667
+ for (let j = 0; j < count; j++) {
14668
+ if (nextScale !== 0) {
14669
+ deltaScale = reader.readEG();
14670
+ nextScale = (lastScale + deltaScale + 256) % 256;
14483
14671
  }
14484
- // here we have state either equal to 2 or 3
14485
- if (!value) {
14486
- state = 3;
14487
- } else if (value === 1) {
14488
- overflow = i - state - 1;
14489
- if (lastUnitStart >= 0) {
14490
- const unit = {
14491
- data: array.subarray(lastUnitStart, overflow),
14492
- type: lastUnitType
14493
- };
14494
- // logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
14495
- units.push(unit);
14496
- } else {
14497
- // lastUnitStart is undefined => this is the first start code found in this PES packet
14498
- // first check if start code delimiter is overlapping between 2 PES packets,
14499
- // ie it started in last packet (lastState not zero)
14500
- // and ended at the beginning of this PES packet (i <= 4 - lastState)
14501
- const lastUnit = this.getLastNalUnit(track.samples);
14502
- if (lastUnit) {
14503
- if (lastState && i <= 4 - lastState) {
14504
- // start delimiter overlapping between PES packets
14505
- // strip start delimiter bytes from the end of last NAL unit
14506
- // check if lastUnit had a state different from zero
14507
- if (lastUnit.state) {
14508
- // strip last bytes
14509
- lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState);
14510
- }
14511
- }
14512
- // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
14672
+ lastScale = nextScale === 0 ? lastScale : nextScale;
14673
+ }
14674
+ }
14513
14675
 
14514
- if (overflow > 0) {
14515
- // logger.log('first NALU found with overflow:' + overflow);
14516
- lastUnit.data = appendUint8Array(lastUnit.data, array.subarray(0, overflow));
14517
- lastUnit.state = 0;
14518
- }
14519
- }
14520
- }
14521
- // check if we can read unit type
14522
- if (i < len) {
14523
- unitType = array[i] & 0x1f;
14524
- // logger.log('find NALU @ offset:' + i + ',type:' + unitType);
14525
- lastUnitStart = i;
14526
- lastUnitType = unitType;
14527
- state = 0;
14528
- } else {
14529
- // not enough byte to read unit type. let's read it on next PES parsing
14530
- state = -1;
14676
+ /**
14677
+ * Read a sequence parameter set and return some interesting video
14678
+ * properties. A sequence parameter set is the H264 metadata that
14679
+ * describes the properties of upcoming video frames.
14680
+ * @returns an object with configuration parsed from the
14681
+ * sequence parameter set, including the dimensions of the
14682
+ * associated video frames.
14683
+ */
14684
+ readSPS(sps) {
14685
+ const eg = new ExpGolomb(sps);
14686
+ let frameCropLeftOffset = 0;
14687
+ let frameCropRightOffset = 0;
14688
+ let frameCropTopOffset = 0;
14689
+ let frameCropBottomOffset = 0;
14690
+ let numRefFramesInPicOrderCntCycle;
14691
+ let scalingListCount;
14692
+ let i;
14693
+ const readUByte = eg.readUByte.bind(eg);
14694
+ const readBits = eg.readBits.bind(eg);
14695
+ const readUEG = eg.readUEG.bind(eg);
14696
+ const readBoolean = eg.readBoolean.bind(eg);
14697
+ const skipBits = eg.skipBits.bind(eg);
14698
+ const skipEG = eg.skipEG.bind(eg);
14699
+ const skipUEG = eg.skipUEG.bind(eg);
14700
+ const skipScalingList = this.skipScalingList.bind(this);
14701
+ readUByte();
14702
+ const profileIdc = readUByte(); // profile_idc
14703
+ readBits(5); // profileCompat constraint_set[0-4]_flag, u(5)
14704
+ skipBits(3); // reserved_zero_3bits u(3),
14705
+ readUByte(); // level_idc u(8)
14706
+ skipUEG(); // seq_parameter_set_id
14707
+ // some profiles have more optional data we don't need
14708
+ if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) {
14709
+ const chromaFormatIdc = readUEG();
14710
+ if (chromaFormatIdc === 3) {
14711
+ skipBits(1);
14712
+ } // separate_colour_plane_flag
14713
+
14714
+ skipUEG(); // bit_depth_luma_minus8
14715
+ skipUEG(); // bit_depth_chroma_minus8
14716
+ skipBits(1); // qpprime_y_zero_transform_bypass_flag
14717
+ if (readBoolean()) {
14718
+ // seq_scaling_matrix_present_flag
14719
+ scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
14720
+ for (i = 0; i < scalingListCount; i++) {
14721
+ if (readBoolean()) {
14722
+ // seq_scaling_list_present_flag[ i ]
14723
+ if (i < 6) {
14724
+ skipScalingList(16, eg);
14725
+ } else {
14726
+ skipScalingList(64, eg);
14727
+ }
14728
+ }
14531
14729
  }
14532
- } else {
14533
- state = 0;
14534
14730
  }
14535
14731
  }
14536
- if (lastUnitStart >= 0 && state >= 0) {
14537
- const unit = {
14538
- data: array.subarray(lastUnitStart, len),
14539
- type: lastUnitType,
14540
- state: state
14541
- };
14542
- units.push(unit);
14543
- // logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state);
14732
+ skipUEG(); // log2_max_frame_num_minus4
14733
+ const picOrderCntType = readUEG();
14734
+ if (picOrderCntType === 0) {
14735
+ readUEG(); // log2_max_pic_order_cnt_lsb_minus4
14736
+ } else if (picOrderCntType === 1) {
14737
+ skipBits(1); // delta_pic_order_always_zero_flag
14738
+ skipEG(); // offset_for_non_ref_pic
14739
+ skipEG(); // offset_for_top_to_bottom_field
14740
+ numRefFramesInPicOrderCntCycle = readUEG();
14741
+ for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
14742
+ skipEG();
14743
+ } // offset_for_ref_frame[ i ]
14544
14744
  }
14545
- // no NALu found
14546
- if (units.length === 0) {
14547
- // append pes.data to previous NAL unit
14548
- const lastUnit = this.getLastNalUnit(track.samples);
14549
- if (lastUnit) {
14550
- lastUnit.data = appendUint8Array(lastUnit.data, array);
14745
+ skipUEG(); // max_num_ref_frames
14746
+ skipBits(1); // gaps_in_frame_num_value_allowed_flag
14747
+ const picWidthInMbsMinus1 = readUEG();
14748
+ const picHeightInMapUnitsMinus1 = readUEG();
14749
+ const frameMbsOnlyFlag = readBits(1);
14750
+ if (frameMbsOnlyFlag === 0) {
14751
+ skipBits(1);
14752
+ } // mb_adaptive_frame_field_flag
14753
+
14754
+ skipBits(1); // direct_8x8_inference_flag
14755
+ if (readBoolean()) {
14756
+ // frame_cropping_flag
14757
+ frameCropLeftOffset = readUEG();
14758
+ frameCropRightOffset = readUEG();
14759
+ frameCropTopOffset = readUEG();
14760
+ frameCropBottomOffset = readUEG();
14761
+ }
14762
+ let pixelRatio = [1, 1];
14763
+ if (readBoolean()) {
14764
+ // vui_parameters_present_flag
14765
+ if (readBoolean()) {
14766
+ // aspect_ratio_info_present_flag
14767
+ const aspectRatioIdc = readUByte();
14768
+ switch (aspectRatioIdc) {
14769
+ case 1:
14770
+ pixelRatio = [1, 1];
14771
+ break;
14772
+ case 2:
14773
+ pixelRatio = [12, 11];
14774
+ break;
14775
+ case 3:
14776
+ pixelRatio = [10, 11];
14777
+ break;
14778
+ case 4:
14779
+ pixelRatio = [16, 11];
14780
+ break;
14781
+ case 5:
14782
+ pixelRatio = [40, 33];
14783
+ break;
14784
+ case 6:
14785
+ pixelRatio = [24, 11];
14786
+ break;
14787
+ case 7:
14788
+ pixelRatio = [20, 11];
14789
+ break;
14790
+ case 8:
14791
+ pixelRatio = [32, 11];
14792
+ break;
14793
+ case 9:
14794
+ pixelRatio = [80, 33];
14795
+ break;
14796
+ case 10:
14797
+ pixelRatio = [18, 11];
14798
+ break;
14799
+ case 11:
14800
+ pixelRatio = [15, 11];
14801
+ break;
14802
+ case 12:
14803
+ pixelRatio = [64, 33];
14804
+ break;
14805
+ case 13:
14806
+ pixelRatio = [160, 99];
14807
+ break;
14808
+ case 14:
14809
+ pixelRatio = [4, 3];
14810
+ break;
14811
+ case 15:
14812
+ pixelRatio = [3, 2];
14813
+ break;
14814
+ case 16:
14815
+ pixelRatio = [2, 1];
14816
+ break;
14817
+ case 255:
14818
+ {
14819
+ pixelRatio = [readUByte() << 8 | readUByte(), readUByte() << 8 | readUByte()];
14820
+ break;
14821
+ }
14822
+ }
14551
14823
  }
14552
14824
  }
14553
- track.naluState = state;
14554
- return units;
14825
+ return {
14826
+ width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2),
14827
+ height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset),
14828
+ pixelRatio: pixelRatio
14829
+ };
14555
14830
  }
14556
14831
  }
14557
14832
 
@@ -14569,7 +14844,7 @@ class SampleAesDecrypter {
14569
14844
  });
14570
14845
  }
14571
14846
  decryptBuffer(encryptedData) {
14572
- return this.decrypter.decrypt(encryptedData, this.keyData.key.buffer, this.keyData.iv.buffer);
14847
+ return this.decrypter.decrypt(encryptedData, this.keyData.key.buffer, this.keyData.iv.buffer, DecrypterAesMode.cbc);
14573
14848
  }
14574
14849
 
14575
14850
  // AAC - encrypt all full 16 bytes blocks starting from offset 16
@@ -14683,7 +14958,7 @@ class TSDemuxer {
14683
14958
  this.observer = observer;
14684
14959
  this.config = config;
14685
14960
  this.typeSupported = typeSupported;
14686
- this.videoParser = new AvcVideoParser();
14961
+ this.videoParser = null;
14687
14962
  }
14688
14963
  static probe(data) {
14689
14964
  const syncOffset = TSDemuxer.syncOffset(data);
@@ -14848,7 +15123,16 @@ class TSDemuxer {
14848
15123
  case videoPid:
14849
15124
  if (stt) {
14850
15125
  if (videoData && (pes = parsePES(videoData))) {
14851
- this.videoParser.parseAVCPES(videoTrack, textTrack, pes, false, this._duration);
15126
+ if (this.videoParser === null) {
15127
+ switch (videoTrack.segmentCodec) {
15128
+ case 'avc':
15129
+ this.videoParser = new AvcVideoParser();
15130
+ break;
15131
+ }
15132
+ }
15133
+ if (this.videoParser !== null) {
15134
+ this.videoParser.parsePES(videoTrack, textTrack, pes, false, this._duration);
15135
+ }
14852
15136
  }
14853
15137
  videoData = {
14854
15138
  data: [],
@@ -15010,8 +15294,17 @@ class TSDemuxer {
15010
15294
  // try to parse last PES packets
15011
15295
  let pes;
15012
15296
  if (videoData && (pes = parsePES(videoData))) {
15013
- this.videoParser.parseAVCPES(videoTrack, textTrack, pes, true, this._duration);
15014
- videoTrack.pesData = null;
15297
+ if (this.videoParser === null) {
15298
+ switch (videoTrack.segmentCodec) {
15299
+ case 'avc':
15300
+ this.videoParser = new AvcVideoParser();
15301
+ break;
15302
+ }
15303
+ }
15304
+ if (this.videoParser !== null) {
15305
+ this.videoParser.parsePES(videoTrack, textTrack, pes, true, this._duration);
15306
+ videoTrack.pesData = null;
15307
+ }
15015
15308
  } else {
15016
15309
  // either avcData null or PES truncated, keep it for next frag parsing
15017
15310
  videoTrack.pesData = videoData;
@@ -15314,7 +15607,10 @@ function parsePMT(data, offset, typeSupported, isSampleAes) {
15314
15607
  logger.warn('Unsupported EC-3 in M2TS found');
15315
15608
  break;
15316
15609
  case 0x24:
15317
- logger.warn('Unsupported HEVC in M2TS found');
15610
+ // ITU-T Rec. H.265 and ISO/IEC 23008-2 (HEVC)
15611
+ {
15612
+ logger.warn('Unsupported HEVC in M2TS found');
15613
+ }
15318
15614
  break;
15319
15615
  }
15320
15616
  // move to the next table entry
@@ -15537,6 +15833,8 @@ class MP4 {
15537
15833
  avc1: [],
15538
15834
  // codingname
15539
15835
  avcC: [],
15836
+ hvc1: [],
15837
+ hvcC: [],
15540
15838
  btrt: [],
15541
15839
  dinf: [],
15542
15840
  dref: [],
@@ -15961,8 +16259,10 @@ class MP4 {
15961
16259
  return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ac3(track));
15962
16260
  }
15963
16261
  return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track));
15964
- } else {
16262
+ } else if (track.segmentCodec === 'avc') {
15965
16263
  return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track));
16264
+ } else {
16265
+ return MP4.box(MP4.types.stsd, MP4.STSD, MP4.hvc1(track));
15966
16266
  }
15967
16267
  }
15968
16268
  static tkhd(track) {
@@ -16100,6 +16400,84 @@ class MP4 {
16100
16400
  const result = appendUint8Array(MP4.FTYP, movie);
16101
16401
  return result;
16102
16402
  }
16403
+ static hvc1(track) {
16404
+ const ps = track.params;
16405
+ const units = [track.vps, track.sps, track.pps];
16406
+ const NALuLengthSize = 4;
16407
+ const config = new Uint8Array([0x01, ps.general_profile_space << 6 | (ps.general_tier_flag ? 32 : 0) | ps.general_profile_idc, ps.general_profile_compatibility_flags[0], ps.general_profile_compatibility_flags[1], ps.general_profile_compatibility_flags[2], ps.general_profile_compatibility_flags[3], ps.general_constraint_indicator_flags[0], ps.general_constraint_indicator_flags[1], ps.general_constraint_indicator_flags[2], ps.general_constraint_indicator_flags[3], ps.general_constraint_indicator_flags[4], ps.general_constraint_indicator_flags[5], ps.general_level_idc, 240 | ps.min_spatial_segmentation_idc >> 8, 255 & ps.min_spatial_segmentation_idc, 252 | ps.parallelismType, 252 | ps.chroma_format_idc, 248 | ps.bit_depth_luma_minus8, 248 | ps.bit_depth_chroma_minus8, 0x00, parseInt(ps.frame_rate.fps), NALuLengthSize - 1 | ps.temporal_id_nested << 2 | ps.num_temporal_layers << 3 | (ps.frame_rate.fixed ? 64 : 0), units.length]);
16408
+
16409
+ // compute hvcC size in bytes
16410
+ let length = config.length;
16411
+ for (let i = 0; i < units.length; i += 1) {
16412
+ length += 3;
16413
+ for (let j = 0; j < units[i].length; j += 1) {
16414
+ length += 2 + units[i][j].length;
16415
+ }
16416
+ }
16417
+ const hvcC = new Uint8Array(length);
16418
+ hvcC.set(config, 0);
16419
+ length = config.length;
16420
+ // append parameter set units: one vps, one or more sps and pps
16421
+ const iMax = units.length - 1;
16422
+ for (let i = 0; i < units.length; i += 1) {
16423
+ hvcC.set(new Uint8Array([32 + i | (i === iMax ? 128 : 0), 0x00, units[i].length]), length);
16424
+ length += 3;
16425
+ for (let j = 0; j < units[i].length; j += 1) {
16426
+ hvcC.set(new Uint8Array([units[i][j].length >> 8, units[i][j].length & 255]), length);
16427
+ length += 2;
16428
+ hvcC.set(units[i][j], length);
16429
+ length += units[i][j].length;
16430
+ }
16431
+ }
16432
+ const hvcc = MP4.box(MP4.types.hvcC, hvcC);
16433
+ const width = track.width;
16434
+ const height = track.height;
16435
+ const hSpacing = track.pixelRatio[0];
16436
+ const vSpacing = track.pixelRatio[1];
16437
+ return MP4.box(MP4.types.hvc1, new Uint8Array([0x00, 0x00, 0x00,
16438
+ // reserved
16439
+ 0x00, 0x00, 0x00,
16440
+ // reserved
16441
+ 0x00, 0x01,
16442
+ // data_reference_index
16443
+ 0x00, 0x00,
16444
+ // pre_defined
16445
+ 0x00, 0x00,
16446
+ // reserved
16447
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
16448
+ // pre_defined
16449
+ width >> 8 & 0xff, width & 0xff,
16450
+ // width
16451
+ height >> 8 & 0xff, height & 0xff,
16452
+ // height
16453
+ 0x00, 0x48, 0x00, 0x00,
16454
+ // horizresolution
16455
+ 0x00, 0x48, 0x00, 0x00,
16456
+ // vertresolution
16457
+ 0x00, 0x00, 0x00, 0x00,
16458
+ // reserved
16459
+ 0x00, 0x01,
16460
+ // frame_count
16461
+ 0x12, 0x64, 0x61, 0x69, 0x6c,
16462
+ // dailymotion/hls.js
16463
+ 0x79, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x68, 0x6c, 0x73, 0x2e, 0x6a, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
16464
+ // compressorname
16465
+ 0x00, 0x18,
16466
+ // depth = 24
16467
+ 0x11, 0x11]),
16468
+ // pre_defined = -1
16469
+ hvcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80,
16470
+ // bufferSizeDB
16471
+ 0x00, 0x2d, 0xc6, 0xc0,
16472
+ // maxBitrate
16473
+ 0x00, 0x2d, 0xc6, 0xc0])),
16474
+ // avgBitrate
16475
+ MP4.box(MP4.types.pasp, new Uint8Array([hSpacing >> 24,
16476
+ // hSpacing
16477
+ hSpacing >> 16 & 0xff, hSpacing >> 8 & 0xff, hSpacing & 0xff, vSpacing >> 24,
16478
+ // vSpacing
16479
+ vSpacing >> 16 & 0xff, vSpacing >> 8 & 0xff, vSpacing & 0xff])));
16480
+ }
16103
16481
  }
16104
16482
  MP4.types = void 0;
16105
16483
  MP4.HDLR_TYPES = void 0;
@@ -16475,9 +16853,9 @@ class MP4Remuxer {
16475
16853
  const foundOverlap = delta < -1;
16476
16854
  if (foundHole || foundOverlap) {
16477
16855
  if (foundHole) {
16478
- logger.warn(`AVC: ${toMsFromMpegTsClock(delta, true)} ms (${delta}dts) hole between fragments detected at ${timeOffset.toFixed(3)}`);
16856
+ logger.warn(`${(track.segmentCodec || '').toUpperCase()}: ${toMsFromMpegTsClock(delta, true)} ms (${delta}dts) hole between fragments detected at ${timeOffset.toFixed(3)}`);
16479
16857
  } else {
16480
- logger.warn(`AVC: ${toMsFromMpegTsClock(-delta, true)} ms (${delta}dts) overlapping between fragments detected at ${timeOffset.toFixed(3)}`);
16858
+ logger.warn(`${(track.segmentCodec || '').toUpperCase()}: ${toMsFromMpegTsClock(-delta, true)} ms (${delta}dts) overlapping between fragments detected at ${timeOffset.toFixed(3)}`);
16481
16859
  }
16482
16860
  if (!foundOverlap || nextAvcDts >= inputSamples[0].pts || chromeVersion) {
16483
16861
  firstDTS = nextAvcDts;
@@ -16486,12 +16864,24 @@ class MP4Remuxer {
16486
16864
  inputSamples[0].dts = firstDTS;
16487
16865
  inputSamples[0].pts = firstPTS;
16488
16866
  } else {
16867
+ let isPTSOrderRetained = true;
16489
16868
  for (let i = 0; i < inputSamples.length; i++) {
16490
- if (inputSamples[i].dts > firstPTS) {
16869
+ if (inputSamples[i].dts > firstPTS && isPTSOrderRetained) {
16491
16870
  break;
16492
16871
  }
16872
+ const prevPTS = inputSamples[i].pts;
16493
16873
  inputSamples[i].dts -= delta;
16494
16874
  inputSamples[i].pts -= delta;
16875
+
16876
+ // check to see if this sample's PTS order has changed
16877
+ // relative to the next one
16878
+ if (i < inputSamples.length - 1) {
16879
+ const nextSamplePTS = inputSamples[i + 1].pts;
16880
+ const currentSamplePTS = inputSamples[i].pts;
16881
+ const currentOrder = nextSamplePTS <= currentSamplePTS;
16882
+ const prevOrder = nextSamplePTS <= prevPTS;
16883
+ isPTSOrderRetained = currentOrder == prevOrder;
16884
+ }
16495
16885
  }
16496
16886
  }
16497
16887
  logger.log(`Video: Initial PTS/DTS adjusted: ${toMsFromMpegTsClock(firstPTS, true)}/${toMsFromMpegTsClock(firstDTS, true)}, delta: ${toMsFromMpegTsClock(delta, true)} ms`);
@@ -16639,7 +17029,7 @@ class MP4Remuxer {
16639
17029
  }
16640
17030
  }
16641
17031
  }
16642
- // next AVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale)
17032
+ // next AVC/HEVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale)
16643
17033
  mp4SampleDuration = stretchedLastFrame || !mp4SampleDuration ? averageSampleDuration : mp4SampleDuration;
16644
17034
  this.nextAvcDts = nextAvcDts = lastDTS + mp4SampleDuration;
16645
17035
  this.videoSampleDuration = mp4SampleDuration;
@@ -16772,7 +17162,7 @@ class MP4Remuxer {
16772
17162
  logger.warn(`[mp4-remuxer]: Injecting ${missing} audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round(1000 * delta / inputTimeScale)} ms gap.`);
16773
17163
  for (let j = 0; j < missing; j++) {
16774
17164
  const newStamp = Math.max(nextPts, 0);
16775
- let fillFrame = AAC.getSilentFrame(track.manifestCodec || track.codec, track.channelCount);
17165
+ let fillFrame = AAC.getSilentFrame(track.parsedCodec || track.manifestCodec || track.codec, track.channelCount);
16776
17166
  if (!fillFrame) {
16777
17167
  logger.log('[mp4-remuxer]: Unable to get silent frame for given audio codec; duplicating last frame instead.');
16778
17168
  fillFrame = sample.unit.subarray();
@@ -16900,7 +17290,7 @@ class MP4Remuxer {
16900
17290
  // samples count of this segment's duration
16901
17291
  const nbSamples = Math.ceil((endDTS - startDTS) / frameDuration);
16902
17292
  // silent frame
16903
- const silentFrame = AAC.getSilentFrame(track.manifestCodec || track.codec, track.channelCount);
17293
+ const silentFrame = AAC.getSilentFrame(track.parsedCodec || track.manifestCodec || track.codec, track.channelCount);
16904
17294
  logger.warn('[mp4-remuxer]: remux empty Audio');
16905
17295
  // Can't remux if we can't generate a silent frame...
16906
17296
  if (!silentFrame) {
@@ -17291,13 +17681,15 @@ class Transmuxer {
17291
17681
  initSegmentData
17292
17682
  } = transmuxConfig;
17293
17683
  const keyData = getEncryptionType(uintData, decryptdata);
17294
- if (keyData && keyData.method === 'AES-128') {
17684
+ if (keyData && isFullSegmentEncryption(keyData.method)) {
17295
17685
  const decrypter = this.getDecrypter();
17686
+ const aesMode = getAesModeFromFullSegmentMethod(keyData.method);
17687
+
17296
17688
  // Software decryption is synchronous; webCrypto is not
17297
17689
  if (decrypter.isSync()) {
17298
17690
  // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
17299
17691
  // data is handled in the flush() call
17300
- let decryptedData = decrypter.softwareDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer);
17692
+ let decryptedData = decrypter.softwareDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer, aesMode);
17301
17693
  // For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress
17302
17694
  const loadingParts = chunkMeta.part > -1;
17303
17695
  if (loadingParts) {
@@ -17309,7 +17701,7 @@ class Transmuxer {
17309
17701
  }
17310
17702
  uintData = new Uint8Array(decryptedData);
17311
17703
  } else {
17312
- this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer).then(decryptedData => {
17704
+ this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer, aesMode).then(decryptedData => {
17313
17705
  // Calling push here is important; if flush() is called while this is still resolving, this ensures that
17314
17706
  // the decrypted data has been transmuxed
17315
17707
  const result = this.push(decryptedData, null, chunkMeta);
@@ -17963,14 +18355,7 @@ class TransmuxerInterface {
17963
18355
  this.observer = new EventEmitter();
17964
18356
  this.observer.on(Events.FRAG_DECRYPTED, forwardMessage);
17965
18357
  this.observer.on(Events.ERROR, forwardMessage);
17966
- const MediaSource = getMediaSource(config.preferManagedMediaSource) || {
17967
- isTypeSupported: () => false
17968
- };
17969
- const m2tsTypeSupported = {
17970
- mpeg: MediaSource.isTypeSupported('audio/mpeg'),
17971
- mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
17972
- ac3: false
17973
- };
18358
+ const m2tsTypeSupported = getM2TSSupportedAudioTypes(config.preferManagedMediaSource);
17974
18359
 
17975
18360
  // navigator.vendor is not always available in Web Worker
17976
18361
  // refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
@@ -18234,8 +18619,9 @@ const STALL_MINIMUM_DURATION_MS = 250;
18234
18619
  const MAX_START_GAP_JUMP = 2.0;
18235
18620
  const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
18236
18621
  const SKIP_BUFFER_RANGE_START = 0.05;
18237
- class GapController {
18622
+ class GapController extends Logger {
18238
18623
  constructor(config, media, fragmentTracker, hls) {
18624
+ super('gap-controller', hls.logger);
18239
18625
  this.config = void 0;
18240
18626
  this.media = null;
18241
18627
  this.fragmentTracker = void 0;
@@ -18245,6 +18631,7 @@ class GapController {
18245
18631
  this.stalled = null;
18246
18632
  this.moved = false;
18247
18633
  this.seeking = false;
18634
+ this.ended = 0;
18248
18635
  this.config = config;
18249
18636
  this.media = media;
18250
18637
  this.fragmentTracker = fragmentTracker;
@@ -18262,7 +18649,7 @@ class GapController {
18262
18649
  *
18263
18650
  * @param lastCurrentTime - Previously read playhead position
18264
18651
  */
18265
- poll(lastCurrentTime, activeFrag) {
18652
+ poll(lastCurrentTime, activeFrag, levelDetails, state) {
18266
18653
  const {
18267
18654
  config,
18268
18655
  media,
@@ -18281,6 +18668,7 @@ class GapController {
18281
18668
 
18282
18669
  // The playhead is moving, no-op
18283
18670
  if (currentTime !== lastCurrentTime) {
18671
+ this.ended = 0;
18284
18672
  this.moved = true;
18285
18673
  if (!seeking) {
18286
18674
  this.nudgeRetry = 0;
@@ -18289,7 +18677,7 @@ class GapController {
18289
18677
  // The playhead is now moving, but was previously stalled
18290
18678
  if (this.stallReported) {
18291
18679
  const _stalledDuration = self.performance.now() - stalled;
18292
- logger.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(_stalledDuration)}ms`);
18680
+ this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(_stalledDuration)}ms`);
18293
18681
  this.stallReported = false;
18294
18682
  }
18295
18683
  this.stalled = null;
@@ -18325,7 +18713,6 @@ class GapController {
18325
18713
  // Skip start gaps if we haven't played, but the last poll detected the start of a stall
18326
18714
  // The addition poll gives the browser a chance to jump the gap for us
18327
18715
  if (!this.moved && this.stalled !== null) {
18328
- var _level$details;
18329
18716
  // There is no playable buffer (seeked, waiting for buffer)
18330
18717
  const isBuffered = bufferInfo.len > 0;
18331
18718
  if (!isBuffered && !nextStart) {
@@ -18337,9 +18724,8 @@ class GapController {
18337
18724
  // When joining a live stream with audio tracks, account for live playlist window sliding by allowing
18338
18725
  // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
18339
18726
  // that begins over 1 target duration after the video start position.
18340
- const level = this.hls.levels ? this.hls.levels[this.hls.currentLevel] : null;
18341
- const isLive = level == null ? void 0 : (_level$details = level.details) == null ? void 0 : _level$details.live;
18342
- const maxStartGapJump = isLive ? level.details.targetduration * 2 : MAX_START_GAP_JUMP;
18727
+ const isLive = !!(levelDetails != null && levelDetails.live);
18728
+ const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
18343
18729
  const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
18344
18730
  if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
18345
18731
  if (!media.paused) {
@@ -18357,6 +18743,17 @@ class GapController {
18357
18743
  }
18358
18744
  const stalledDuration = tnow - stalled;
18359
18745
  if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
18746
+ // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
18747
+ if (state === State.ENDED && !(levelDetails && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? void 0 : levelDetails.edge) || 0)) < 1) {
18748
+ if (stalledDuration < 1000 || this.ended) {
18749
+ return;
18750
+ }
18751
+ this.ended = currentTime;
18752
+ this.hls.trigger(Events.MEDIA_ENDED, {
18753
+ stalled: true
18754
+ });
18755
+ return;
18756
+ }
18360
18757
  // Report stalling after trying to fix
18361
18758
  this._reportStall(bufferInfo);
18362
18759
  if (!this.media) {
@@ -18400,7 +18797,7 @@ class GapController {
18400
18797
  // needs to cross some sort of threshold covering all source-buffers content
18401
18798
  // to start playing properly.
18402
18799
  if ((bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
18403
- logger.warn('Trying to nudge playhead over buffer-hole');
18800
+ this.warn('Trying to nudge playhead over buffer-hole');
18404
18801
  // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
18405
18802
  // We only try to jump the hole if it's under the configured size
18406
18803
  // Reset stalled so to rearm watchdog timer
@@ -18424,7 +18821,7 @@ class GapController {
18424
18821
  // Report stalled error once
18425
18822
  this.stallReported = true;
18426
18823
  const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
18427
- logger.warn(error.message);
18824
+ this.warn(error.message);
18428
18825
  hls.trigger(Events.ERROR, {
18429
18826
  type: ErrorTypes.MEDIA_ERROR,
18430
18827
  details: ErrorDetails.BUFFER_STALLED_ERROR,
@@ -18492,7 +18889,7 @@ class GapController {
18492
18889
  }
18493
18890
  }
18494
18891
  const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
18495
- logger.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
18892
+ this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
18496
18893
  this.moved = true;
18497
18894
  this.stalled = null;
18498
18895
  media.currentTime = targetTime;
@@ -18533,7 +18930,7 @@ class GapController {
18533
18930
  const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset;
18534
18931
  // playback stalled in buffered area ... let's nudge currentTime to try to overcome this
18535
18932
  const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`);
18536
- logger.warn(error.message);
18933
+ this.warn(error.message);
18537
18934
  media.currentTime = targetTime;
18538
18935
  hls.trigger(Events.ERROR, {
18539
18936
  type: ErrorTypes.MEDIA_ERROR,
@@ -18543,7 +18940,7 @@ class GapController {
18543
18940
  });
18544
18941
  } else {
18545
18942
  const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
18546
- logger.error(error.message);
18943
+ this.error(error.message);
18547
18944
  hls.trigger(Events.ERROR, {
18548
18945
  type: ErrorTypes.MEDIA_ERROR,
18549
18946
  details: ErrorDetails.BUFFER_STALLED_ERROR,
@@ -18558,7 +18955,7 @@ const TICK_INTERVAL = 100; // how often to tick in ms
18558
18955
 
18559
18956
  class StreamController extends BaseStreamController {
18560
18957
  constructor(hls, fragmentTracker, keyLoader) {
18561
- super(hls, fragmentTracker, keyLoader, '[stream-controller]', PlaylistLevelType.MAIN);
18958
+ super(hls, fragmentTracker, keyLoader, 'stream-controller', PlaylistLevelType.MAIN);
18562
18959
  this.audioCodecSwap = false;
18563
18960
  this.gapController = null;
18564
18961
  this.level = -1;
@@ -18566,27 +18963,43 @@ class StreamController extends BaseStreamController {
18566
18963
  this.altAudio = false;
18567
18964
  this.audioOnly = false;
18568
18965
  this.fragPlaying = null;
18569
- this.onvplaying = null;
18570
- this.onvseeked = null;
18571
18966
  this.fragLastKbps = 0;
18572
18967
  this.couldBacktrack = false;
18573
18968
  this.backtrackFragment = null;
18574
18969
  this.audioCodecSwitch = false;
18575
18970
  this.videoBuffer = null;
18576
- this._registerListeners();
18971
+ this.onMediaPlaying = () => {
18972
+ // tick to speed up FRAG_CHANGED triggering
18973
+ this.tick();
18974
+ };
18975
+ this.onMediaSeeked = () => {
18976
+ const media = this.media;
18977
+ const currentTime = media ? media.currentTime : null;
18978
+ if (isFiniteNumber(currentTime)) {
18979
+ this.log(`Media seeked to ${currentTime.toFixed(3)}`);
18980
+ }
18981
+
18982
+ // If seeked was issued before buffer was appended do not tick immediately
18983
+ const bufferInfo = this.getMainFwdBufferInfo();
18984
+ if (bufferInfo === null || bufferInfo.len === 0) {
18985
+ this.warn(`Main forward buffer length on "seeked" event ${bufferInfo ? bufferInfo.len : 'empty'})`);
18986
+ return;
18987
+ }
18988
+
18989
+ // tick to speed up FRAG_CHANGED triggering
18990
+ this.tick();
18991
+ };
18992
+ this.registerListeners();
18577
18993
  }
18578
- _registerListeners() {
18994
+ registerListeners() {
18995
+ super.registerListeners();
18579
18996
  const {
18580
18997
  hls
18581
18998
  } = this;
18582
- hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
18583
- hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
18584
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
18585
18999
  hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
18586
19000
  hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
18587
19001
  hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
18588
19002
  hls.on(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this);
18589
- hls.on(Events.ERROR, this.onError, this);
18590
19003
  hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
18591
19004
  hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
18592
19005
  hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this);
@@ -18594,17 +19007,14 @@ class StreamController extends BaseStreamController {
18594
19007
  hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
18595
19008
  hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
18596
19009
  }
18597
- _unregisterListeners() {
19010
+ unregisterListeners() {
19011
+ super.unregisterListeners();
18598
19012
  const {
18599
19013
  hls
18600
19014
  } = this;
18601
- hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
18602
- hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
18603
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
18604
19015
  hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
18605
19016
  hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
18606
19017
  hls.off(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this);
18607
- hls.off(Events.ERROR, this.onError, this);
18608
19018
  hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
18609
19019
  hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
18610
19020
  hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this);
@@ -18613,7 +19023,9 @@ class StreamController extends BaseStreamController {
18613
19023
  hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
18614
19024
  }
18615
19025
  onHandlerDestroying() {
18616
- this._unregisterListeners();
19026
+ // @ts-ignore
19027
+ this.onMediaPlaying = this.onMediaSeeked = null;
19028
+ this.unregisterListeners();
18617
19029
  super.onHandlerDestroying();
18618
19030
  }
18619
19031
  startLoad(startPosition) {
@@ -18711,6 +19123,9 @@ class StreamController extends BaseStreamController {
18711
19123
  this.checkFragmentChanged();
18712
19124
  }
18713
19125
  doTickIdle() {
19126
+ if (!this.buffering) {
19127
+ return;
19128
+ }
18714
19129
  const {
18715
19130
  hls,
18716
19131
  levelLastLoaded,
@@ -18938,20 +19353,17 @@ class StreamController extends BaseStreamController {
18938
19353
  onMediaAttached(event, data) {
18939
19354
  super.onMediaAttached(event, data);
18940
19355
  const media = data.media;
18941
- this.onvplaying = this.onMediaPlaying.bind(this);
18942
- this.onvseeked = this.onMediaSeeked.bind(this);
18943
- media.addEventListener('playing', this.onvplaying);
18944
- media.addEventListener('seeked', this.onvseeked);
19356
+ media.addEventListener('playing', this.onMediaPlaying);
19357
+ media.addEventListener('seeked', this.onMediaSeeked);
18945
19358
  this.gapController = new GapController(this.config, media, this.fragmentTracker, this.hls);
18946
19359
  }
18947
19360
  onMediaDetaching() {
18948
19361
  const {
18949
19362
  media
18950
19363
  } = this;
18951
- if (media && this.onvplaying && this.onvseeked) {
18952
- media.removeEventListener('playing', this.onvplaying);
18953
- media.removeEventListener('seeked', this.onvseeked);
18954
- this.onvplaying = this.onvseeked = null;
19364
+ if (media) {
19365
+ media.removeEventListener('playing', this.onMediaPlaying);
19366
+ media.removeEventListener('seeked', this.onMediaSeeked);
18955
19367
  this.videoBuffer = null;
18956
19368
  }
18957
19369
  this.fragPlaying = null;
@@ -18961,27 +19373,6 @@ class StreamController extends BaseStreamController {
18961
19373
  }
18962
19374
  super.onMediaDetaching();
18963
19375
  }
18964
- onMediaPlaying() {
18965
- // tick to speed up FRAG_CHANGED triggering
18966
- this.tick();
18967
- }
18968
- onMediaSeeked() {
18969
- const media = this.media;
18970
- const currentTime = media ? media.currentTime : null;
18971
- if (isFiniteNumber(currentTime)) {
18972
- this.log(`Media seeked to ${currentTime.toFixed(3)}`);
18973
- }
18974
-
18975
- // If seeked was issued before buffer was appended do not tick immediately
18976
- const bufferInfo = this.getMainFwdBufferInfo();
18977
- if (bufferInfo === null || bufferInfo.len === 0) {
18978
- this.warn(`Main forward buffer length on "seeked" event ${bufferInfo ? bufferInfo.len : 'empty'})`);
18979
- return;
18980
- }
18981
-
18982
- // tick to speed up FRAG_CHANGED triggering
18983
- this.tick();
18984
- }
18985
19376
  onManifestLoading() {
18986
19377
  // reset buffer on manifest loading
18987
19378
  this.log('Trigger BUFFER_RESET');
@@ -19273,8 +19664,10 @@ class StreamController extends BaseStreamController {
19273
19664
  }
19274
19665
  if (this.loadedmetadata || !BufferHelper.getBuffered(media).length) {
19275
19666
  // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
19276
- const activeFrag = this.state !== State.IDLE ? this.fragCurrent : null;
19277
- gapController.poll(this.lastCurrentTime, activeFrag);
19667
+ const state = this.state;
19668
+ const activeFrag = state !== State.IDLE ? this.fragCurrent : null;
19669
+ const levelDetails = this.getLevelDetails();
19670
+ gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state);
19278
19671
  }
19279
19672
  this.lastCurrentTime = media.currentTime;
19280
19673
  }
@@ -19607,6 +20000,17 @@ class StreamController extends BaseStreamController {
19607
20000
  getMainFwdBufferInfo() {
19608
20001
  return this.getFwdBufferInfo(this.mediaBuffer ? this.mediaBuffer : this.media, PlaylistLevelType.MAIN);
19609
20002
  }
20003
+ get maxBufferLength() {
20004
+ const {
20005
+ levels,
20006
+ level
20007
+ } = this;
20008
+ const levelInfo = levels == null ? void 0 : levels[level];
20009
+ if (!levelInfo) {
20010
+ return this.config.maxBufferLength;
20011
+ }
20012
+ return this.getMaxBufferLength(levelInfo.maxBitrate);
20013
+ }
19610
20014
  backtrack(frag) {
19611
20015
  this.couldBacktrack = true;
19612
20016
  // Causes findFragments to backtrack through fragments to find the keyframe
@@ -19712,7 +20116,7 @@ class Hls {
19712
20116
  * Get the video-dev/hls.js package version.
19713
20117
  */
19714
20118
  static get version() {
19715
- return "1.5.7";
20119
+ return "1.5.8-0.canary.10046";
19716
20120
  }
19717
20121
 
19718
20122
  /**
@@ -19775,9 +20179,12 @@ class Hls {
19775
20179
  * The configuration object provided on player instantiation.
19776
20180
  */
19777
20181
  this.userConfig = void 0;
20182
+ /**
20183
+ * The logger functions used by this player instance, configured on player instantiation.
20184
+ */
20185
+ this.logger = void 0;
19778
20186
  this.coreComponents = void 0;
19779
20187
  this.networkControllers = void 0;
19780
- this.started = false;
19781
20188
  this._emitter = new EventEmitter();
19782
20189
  this._autoLevelCapping = -1;
19783
20190
  this._maxHdcpLevel = null;
@@ -19794,11 +20201,11 @@ class Hls {
19794
20201
  this._media = null;
19795
20202
  this.url = null;
19796
20203
  this.triggeringException = void 0;
19797
- enableLogs(userConfig.debug || false, 'Hls instance');
19798
- const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig);
20204
+ const logger = this.logger = enableLogs(userConfig.debug || false, 'Hls instance');
20205
+ const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig, logger);
19799
20206
  this.userConfig = userConfig;
19800
20207
  if (config.progressive) {
19801
- enableStreamingMode(config);
20208
+ enableStreamingMode(config, logger);
19802
20209
  }
19803
20210
 
19804
20211
  // core controllers and network loaders
@@ -19811,7 +20218,9 @@ class Hls {
19811
20218
  } = config;
19812
20219
  const errorController = new ConfigErrorController(this);
19813
20220
  const abrController = this.abrController = new ConfigAbrController(this);
19814
- const bufferController = this.bufferController = new ConfigBufferController(this);
20221
+ // FragmentTracker must be defined before StreamController because the order of event handling is important
20222
+ const fragmentTracker = new FragmentTracker(this);
20223
+ const bufferController = this.bufferController = new ConfigBufferController(this, fragmentTracker);
19815
20224
  const capLevelController = this.capLevelController = new ConfigCapLevelController(this);
19816
20225
  const fpsController = new ConfigFpsController(this);
19817
20226
  const playListLoader = new PlaylistLoader(this);
@@ -19820,8 +20229,6 @@ class Hls {
19820
20229
  // ConentSteeringController is defined before LevelController to receive Multivariant Playlist events first
19821
20230
  const contentSteering = ConfigContentSteeringController ? new ConfigContentSteeringController(this) : null;
19822
20231
  const levelController = this.levelController = new LevelController(this, contentSteering);
19823
- // FragmentTracker must be defined before StreamController because the order of event handling is important
19824
- const fragmentTracker = new FragmentTracker(this);
19825
20232
  const keyLoader = new KeyLoader(this.config);
19826
20233
  const streamController = this.streamController = new StreamController(this, fragmentTracker, keyLoader);
19827
20234
 
@@ -19897,7 +20304,7 @@ class Hls {
19897
20304
  try {
19898
20305
  return this.emit(event, event, eventObject);
19899
20306
  } catch (error) {
19900
- logger.error('An internal error happened while handling event ' + event + '. Error message: "' + error.message + '". Here is a stacktrace:', error);
20307
+ this.logger.error('An internal error happened while handling event ' + event + '. Error message: "' + error.message + '". Here is a stacktrace:', error);
19901
20308
  // Prevent recursion in error event handlers that throw #5497
19902
20309
  if (!this.triggeringException) {
19903
20310
  this.triggeringException = true;
@@ -19923,7 +20330,7 @@ class Hls {
19923
20330
  * Dispose of the instance
19924
20331
  */
19925
20332
  destroy() {
19926
- logger.log('destroy');
20333
+ this.logger.log('destroy');
19927
20334
  this.trigger(Events.DESTROYING, undefined);
19928
20335
  this.detachMedia();
19929
20336
  this.removeAllListeners();
@@ -19944,7 +20351,7 @@ class Hls {
19944
20351
  * Attaches Hls.js to a media element
19945
20352
  */
19946
20353
  attachMedia(media) {
19947
- logger.log('attachMedia');
20354
+ this.logger.log('attachMedia');
19948
20355
  this._media = media;
19949
20356
  this.trigger(Events.MEDIA_ATTACHING, {
19950
20357
  media: media
@@ -19955,7 +20362,7 @@ class Hls {
19955
20362
  * Detach Hls.js from the media
19956
20363
  */
19957
20364
  detachMedia() {
19958
- logger.log('detachMedia');
20365
+ this.logger.log('detachMedia');
19959
20366
  this.trigger(Events.MEDIA_DETACHING, undefined);
19960
20367
  this._media = null;
19961
20368
  }
@@ -19972,7 +20379,7 @@ class Hls {
19972
20379
  });
19973
20380
  this._autoLevelCapping = -1;
19974
20381
  this._maxHdcpLevel = null;
19975
- logger.log(`loadSource:${loadingSource}`);
20382
+ this.logger.log(`loadSource:${loadingSource}`);
19976
20383
  if (media && loadedSource && (loadedSource !== loadingSource || this.bufferController.hasSourceTypes())) {
19977
20384
  this.detachMedia();
19978
20385
  this.attachMedia(media);
@@ -19991,8 +20398,7 @@ class Hls {
19991
20398
  * Defaults to -1 (None: starts from earliest point)
19992
20399
  */
19993
20400
  startLoad(startPosition = -1) {
19994
- logger.log(`startLoad(${startPosition})`);
19995
- this.started = true;
20401
+ this.logger.log(`startLoad(${startPosition})`);
19996
20402
  this.networkControllers.forEach(controller => {
19997
20403
  controller.startLoad(startPosition);
19998
20404
  });
@@ -20002,34 +20408,31 @@ class Hls {
20002
20408
  * Stop loading of any stream data.
20003
20409
  */
20004
20410
  stopLoad() {
20005
- logger.log('stopLoad');
20006
- this.started = false;
20411
+ this.logger.log('stopLoad');
20007
20412
  this.networkControllers.forEach(controller => {
20008
20413
  controller.stopLoad();
20009
20414
  });
20010
20415
  }
20011
20416
 
20012
20417
  /**
20013
- * Resumes stream controller segment loading if previously started.
20418
+ * Resumes stream controller segment loading after `pauseBuffering` has been called.
20014
20419
  */
20015
20420
  resumeBuffering() {
20016
- if (this.started) {
20017
- this.networkControllers.forEach(controller => {
20018
- if ('fragmentLoader' in controller) {
20019
- controller.startLoad(-1);
20020
- }
20021
- });
20022
- }
20421
+ this.networkControllers.forEach(controller => {
20422
+ if (controller.resumeBuffering) {
20423
+ controller.resumeBuffering();
20424
+ }
20425
+ });
20023
20426
  }
20024
20427
 
20025
20428
  /**
20026
- * Stops stream controller segment loading without changing 'started' state like stopLoad().
20429
+ * Prevents stream controller from loading new segments until `resumeBuffering` is called.
20027
20430
  * This allows for media buffering to be paused without interupting playlist loading.
20028
20431
  */
20029
20432
  pauseBuffering() {
20030
20433
  this.networkControllers.forEach(controller => {
20031
- if ('fragmentLoader' in controller) {
20032
- controller.stopLoad();
20434
+ if (controller.pauseBuffering) {
20435
+ controller.pauseBuffering();
20033
20436
  }
20034
20437
  });
20035
20438
  }
@@ -20038,7 +20441,7 @@ class Hls {
20038
20441
  * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)
20039
20442
  */
20040
20443
  swapAudioCodec() {
20041
- logger.log('swapAudioCodec');
20444
+ this.logger.log('swapAudioCodec');
20042
20445
  this.streamController.swapAudioCodec();
20043
20446
  }
20044
20447
 
@@ -20049,7 +20452,7 @@ class Hls {
20049
20452
  * Automatic recovery of media-errors by this process is configurable.
20050
20453
  */
20051
20454
  recoverMediaError() {
20052
- logger.log('recoverMediaError');
20455
+ this.logger.log('recoverMediaError');
20053
20456
  const media = this._media;
20054
20457
  this.detachMedia();
20055
20458
  if (media) {
@@ -20079,7 +20482,7 @@ class Hls {
20079
20482
  * Set quality level index immediately. This will flush the current buffer to replace the quality asap. That means playback will interrupt at least shortly to re-buffer and re-sync eventually. Set to -1 for automatic level selection.
20080
20483
  */
20081
20484
  set currentLevel(newLevel) {
20082
- logger.log(`set currentLevel:${newLevel}`);
20485
+ this.logger.log(`set currentLevel:${newLevel}`);
20083
20486
  this.levelController.manualLevel = newLevel;
20084
20487
  this.streamController.immediateLevelSwitch();
20085
20488
  }
@@ -20098,7 +20501,7 @@ class Hls {
20098
20501
  * @param newLevel - Pass -1 for automatic level selection
20099
20502
  */
20100
20503
  set nextLevel(newLevel) {
20101
- logger.log(`set nextLevel:${newLevel}`);
20504
+ this.logger.log(`set nextLevel:${newLevel}`);
20102
20505
  this.levelController.manualLevel = newLevel;
20103
20506
  this.streamController.nextLevelSwitch();
20104
20507
  }
@@ -20117,7 +20520,7 @@ class Hls {
20117
20520
  * @param newLevel - Pass -1 for automatic level selection
20118
20521
  */
20119
20522
  set loadLevel(newLevel) {
20120
- logger.log(`set loadLevel:${newLevel}`);
20523
+ this.logger.log(`set loadLevel:${newLevel}`);
20121
20524
  this.levelController.manualLevel = newLevel;
20122
20525
  }
20123
20526
 
@@ -20148,7 +20551,7 @@ class Hls {
20148
20551
  * Sets "first-level", see getter.
20149
20552
  */
20150
20553
  set firstLevel(newLevel) {
20151
- logger.log(`set firstLevel:${newLevel}`);
20554
+ this.logger.log(`set firstLevel:${newLevel}`);
20152
20555
  this.levelController.firstLevel = newLevel;
20153
20556
  }
20154
20557
 
@@ -20173,7 +20576,7 @@ class Hls {
20173
20576
  * (determined from download of first segment)
20174
20577
  */
20175
20578
  set startLevel(newLevel) {
20176
- logger.log(`set startLevel:${newLevel}`);
20579
+ this.logger.log(`set startLevel:${newLevel}`);
20177
20580
  // if not in automatic start level detection, ensure startLevel is greater than minAutoLevel
20178
20581
  if (newLevel !== -1) {
20179
20582
  newLevel = Math.max(newLevel, this.minAutoLevel);
@@ -20248,7 +20651,7 @@ class Hls {
20248
20651
  */
20249
20652
  set autoLevelCapping(newLevel) {
20250
20653
  if (this._autoLevelCapping !== newLevel) {
20251
- logger.log(`set autoLevelCapping:${newLevel}`);
20654
+ this.logger.log(`set autoLevelCapping:${newLevel}`);
20252
20655
  this._autoLevelCapping = newLevel;
20253
20656
  this.levelController.checkMaxAutoUpdated();
20254
20657
  }
@@ -20353,6 +20756,9 @@ class Hls {
20353
20756
  get mainForwardBufferInfo() {
20354
20757
  return this.streamController.getMainFwdBufferInfo();
20355
20758
  }
20759
+ get maxBufferLength() {
20760
+ return this.streamController.maxBufferLength;
20761
+ }
20356
20762
 
20357
20763
  /**
20358
20764
  * Find and select the best matching audio track, making a level switch when a Group change is necessary.
@@ -20527,5 +20933,5 @@ var KeySystemFormats = empty.KeySystemFormats;
20527
20933
  var KeySystems = empty.KeySystems;
20528
20934
  var SubtitleStreamController = empty.SubtitleStreamController;
20529
20935
  var TimelineController = empty.TimelineController;
20530
- export { AbrController, AttrList, Cues as AudioStreamController, Cues as AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, Cues as CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, DateRange, Cues as EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, Cues as SubtitleTrackController, TimelineController, Hls as default, getMediaSource, isMSESupported, isSupported };
20936
+ export { AbrController, AttrList, HevcVideoParser as AudioStreamController, HevcVideoParser as AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, HevcVideoParser as CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, DateRange, HevcVideoParser as EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, HevcVideoParser as SubtitleTrackController, TimelineController, Hls as default, getMediaSource, isMSESupported, isSupported };
20531
20937
  //# sourceMappingURL=hls.light.mjs.map