hls.js 1.6.0-beta.2.0.canary.10924 → 1.6.0-beta.2.0.canary.10926

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.
package/dist/hls.mjs CHANGED
@@ -402,7 +402,7 @@ function enableLogs(debugConfig, context, id) {
402
402
  // Some browsers don't allow to use bind on console object anyway
403
403
  // fallback to default if needed
404
404
  try {
405
- newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.6.0-beta.2.0.canary.10924"}`);
405
+ newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.6.0-beta.2.0.canary.10926"}`);
406
406
  } catch (e) {
407
407
  /* log fn threw an exception. All logger methods are no-ops. */
408
408
  return createLogger();
@@ -1307,8 +1307,8 @@ function searchDownAndUpList(arr, searchIndex, predicate) {
1307
1307
  return -1;
1308
1308
  }
1309
1309
  function useAlternateAudio(audioTrackUrl, hls) {
1310
- var _hls$levels$hls$loadL;
1311
- return !!audioTrackUrl && audioTrackUrl !== ((_hls$levels$hls$loadL = hls.levels[hls.loadLevel]) == null ? undefined : _hls$levels$hls$loadL.uri);
1310
+ var _hls$loadLevelObj;
1311
+ return !!audioTrackUrl && audioTrackUrl !== ((_hls$loadLevelObj = hls.loadLevelObj) == null ? undefined : _hls$loadLevelObj.uri);
1312
1312
  }
1313
1313
 
1314
1314
  class AbrController extends Logger {
@@ -1802,8 +1802,8 @@ class AbrController extends Logger {
1802
1802
  }
1803
1803
  // If no matching level found, see if min auto level would be a better option
1804
1804
  const minLevel = hls.levels[minAutoLevel];
1805
- const autoLevel = hls.levels[hls.loadLevel];
1806
- if ((minLevel == null ? undefined : minLevel.bitrate) < (autoLevel == null ? undefined : autoLevel.bitrate)) {
1805
+ const autoLevel = hls.loadLevelObj;
1806
+ if (autoLevel && (minLevel == null ? undefined : minLevel.bitrate) < autoLevel.bitrate) {
1807
1807
  return minAutoLevel;
1808
1808
  }
1809
1809
  // or if bitrate is not lower, continue to use loadLevel
@@ -2344,7 +2344,7 @@ class ErrorController extends Logger {
2344
2344
  case ErrorDetails.SUBTITLE_LOAD_ERROR:
2345
2345
  case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT:
2346
2346
  if (context) {
2347
- const level = hls.levels[hls.loadLevel];
2347
+ const level = hls.loadLevelObj;
2348
2348
  if (level && (context.type === PlaylistContextType.AUDIO_TRACK && level.hasAudioGroup(context.groupId) || context.type === PlaylistContextType.SUBTITLE_TRACK && level.hasSubtitleGroup(context.groupId))) {
2349
2349
  // Perform Pathway switch or Redundant failover if possible for fastest recovery
2350
2350
  // otherwise allow playlist retry count to reach max error retries
@@ -2357,7 +2357,7 @@ class ErrorController extends Logger {
2357
2357
  return;
2358
2358
  case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:
2359
2359
  {
2360
- const level = hls.levels[hls.loadLevel];
2360
+ const level = hls.loadLevelObj;
2361
2361
  const restrictedHdcpLevel = level == null ? undefined : level.attrs['HDCP-LEVEL'];
2362
2362
  if (restrictedHdcpLevel) {
2363
2363
  data.errorAction = {
@@ -5845,36 +5845,53 @@ class BufferHelper {
5845
5845
  }
5846
5846
  return false;
5847
5847
  }
5848
+ static bufferedRanges(media) {
5849
+ if (media) {
5850
+ const timeRanges = BufferHelper.getBuffered(media);
5851
+ return BufferHelper.timeRangesToArray(timeRanges);
5852
+ }
5853
+ return [];
5854
+ }
5855
+ static timeRangesToArray(timeRanges) {
5856
+ const buffered = [];
5857
+ for (let i = 0; i < timeRanges.length; i++) {
5858
+ buffered.push({
5859
+ start: timeRanges.start(i),
5860
+ end: timeRanges.end(i)
5861
+ });
5862
+ }
5863
+ return buffered;
5864
+ }
5848
5865
  static bufferInfo(media, pos, maxHoleDuration) {
5849
5866
  if (media) {
5850
- const vbuffered = BufferHelper.getBuffered(media);
5851
- if (vbuffered.length) {
5852
- const buffered = [];
5853
- for (let i = 0; i < vbuffered.length; i++) {
5854
- buffered.push({
5855
- start: vbuffered.start(i),
5856
- end: vbuffered.end(i)
5857
- });
5858
- }
5867
+ const buffered = BufferHelper.bufferedRanges(media);
5868
+ if (buffered.length) {
5859
5869
  return BufferHelper.bufferedInfo(buffered, pos, maxHoleDuration);
5860
5870
  }
5861
5871
  }
5862
5872
  return {
5863
5873
  len: 0,
5864
5874
  start: pos,
5865
- end: pos
5875
+ end: pos,
5876
+ bufferedIndex: -1
5866
5877
  };
5867
5878
  }
5868
5879
  static bufferedInfo(buffered, pos, maxHoleDuration) {
5869
5880
  pos = Math.max(0, pos);
5870
5881
  // sort on buffer.start/smaller end (IE does not always return sorted buffered range)
5871
- buffered.sort((a, b) => a.start - b.start || b.end - a.end);
5882
+ if (buffered.length > 1) {
5883
+ buffered.sort((a, b) => a.start - b.start || b.end - a.end);
5884
+ }
5885
+ let bufferedIndex = -1;
5872
5886
  let buffered2 = [];
5873
5887
  if (maxHoleDuration) {
5874
5888
  // there might be some small holes between buffer time range
5875
5889
  // consider that holes smaller than maxHoleDuration are irrelevant and build another
5876
5890
  // buffer time range representations that discards those holes
5877
5891
  for (let i = 0; i < buffered.length; i++) {
5892
+ if (pos >= buffered[i].start && pos <= buffered[i].end) {
5893
+ bufferedIndex = i;
5894
+ }
5878
5895
  const buf2len = buffered2.length;
5879
5896
  if (buf2len) {
5880
5897
  const buf2end = buffered2[buf2len - 1].end;
@@ -5900,24 +5917,25 @@ class BufferHelper {
5900
5917
  buffered2 = buffered;
5901
5918
  }
5902
5919
  let bufferLen = 0;
5920
+ let nextStart;
5903
5921
 
5904
- // bufferStartNext can possibly be undefined based on the conditional logic below
5905
- let bufferStartNext;
5906
-
5907
- // bufferStart and bufferEnd are buffer boundaries around current video position
5922
+ // bufferStart and bufferEnd are buffer boundaries around current playback position (pos)
5908
5923
  let bufferStart = pos;
5909
5924
  let bufferEnd = pos;
5910
5925
  for (let i = 0; i < buffered2.length; i++) {
5911
5926
  const start = buffered2[i].start;
5912
5927
  const end = buffered2[i].end;
5913
5928
  // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
5929
+ if (bufferedIndex === -1 && pos >= start && pos <= end) {
5930
+ bufferedIndex = i;
5931
+ }
5914
5932
  if (pos + maxHoleDuration >= start && pos < end) {
5915
5933
  // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
5916
5934
  bufferStart = start;
5917
5935
  bufferEnd = end;
5918
5936
  bufferLen = bufferEnd - pos;
5919
5937
  } else if (pos + maxHoleDuration < start) {
5920
- bufferStartNext = start;
5938
+ nextStart = start;
5921
5939
  break;
5922
5940
  }
5923
5941
  }
@@ -5925,8 +5943,9 @@ class BufferHelper {
5925
5943
  len: bufferLen,
5926
5944
  start: bufferStart || 0,
5927
5945
  end: bufferEnd || 0,
5928
- nextStart: bufferStartNext,
5929
- buffered
5946
+ nextStart,
5947
+ buffered,
5948
+ bufferedIndex
5930
5949
  };
5931
5950
  }
5932
5951
 
@@ -8112,7 +8131,6 @@ class BaseStreamController extends TaskLoop {
8112
8131
  // reset startPosition and lastCurrentTime to restart playback @ stream beginning
8113
8132
  this.log(`setting startPosition to 0 because media ended`);
8114
8133
  this.startPosition = this.lastCurrentTime = 0;
8115
- this.triggerEnded();
8116
8134
  };
8117
8135
  this.playlistType = playlistType;
8118
8136
  this.hls = hls;
@@ -8186,6 +8204,12 @@ class BaseStreamController extends TaskLoop {
8186
8204
  resumeBuffering() {
8187
8205
  this.buffering = true;
8188
8206
  }
8207
+ get inFlightFrag() {
8208
+ return {
8209
+ frag: this.fragCurrent,
8210
+ state: this.state
8211
+ };
8212
+ }
8189
8213
  _streamEnded(bufferInfo, levelDetails) {
8190
8214
  // Stream is never "ended" when playlist is live or media is detached
8191
8215
  if (levelDetails.live || !this.media) {
@@ -8274,9 +8298,6 @@ class BaseStreamController extends TaskLoop {
8274
8298
  this.startFragRequested = false;
8275
8299
  }
8276
8300
  onError(event, data) {}
8277
- triggerEnded() {
8278
- /* overridden in stream-controller */
8279
- }
8280
8301
  onManifestLoaded(event, data) {
8281
8302
  this.startTimeOffset = data.startTimeOffset;
8282
8303
  }
@@ -8871,7 +8892,8 @@ class BaseStreamController extends TaskLoop {
8871
8892
  if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
8872
8893
  const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
8873
8894
  if (bufferedFragAtPos && (bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)) {
8874
- return BufferHelper.bufferInfo(bufferable, pos, Math.max(bufferInfo.nextStart, maxBufferHole));
8895
+ const gapDuration = Math.max(Math.min(bufferInfo.nextStart, bufferedFragAtPos.end) - pos, maxBufferHole);
8896
+ return BufferHelper.bufferInfo(bufferable, pos, gapDuration);
8875
8897
  }
8876
8898
  }
8877
8899
  return bufferInfo;
@@ -9838,7 +9860,7 @@ var eventemitter3 = {exports: {}};
9838
9860
  var eventemitter3Exports = eventemitter3.exports;
9839
9861
  var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports);
9840
9862
 
9841
- const version = "1.6.0-beta.2.0.canary.10924";
9863
+ const version = "1.6.0-beta.2.0.canary.10926";
9842
9864
 
9843
9865
  // ensure the worker ends up in the bundle
9844
9866
  // If the worker should not be included this gets aliased to empty.js
@@ -16085,7 +16107,7 @@ class TransmuxerInterface {
16085
16107
  }
16086
16108
  }
16087
16109
 
16088
- const TICK_INTERVAL$2 = 100; // how often to tick in ms
16110
+ const TICK_INTERVAL$3 = 100; // how often to tick in ms
16089
16111
 
16090
16112
  class AudioStreamController extends BaseStreamController {
16091
16113
  constructor(hls, fragmentTracker, keyLoader) {
@@ -16197,7 +16219,7 @@ class AudioStreamController extends BaseStreamController {
16197
16219
  }
16198
16220
  const lastCurrentTime = this.lastCurrentTime;
16199
16221
  this.stopLoad();
16200
- this.setInterval(TICK_INTERVAL$2);
16222
+ this.setInterval(TICK_INTERVAL$3);
16201
16223
  if (lastCurrentTime > 0 && startPosition === -1) {
16202
16224
  this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`);
16203
16225
  startPosition = lastCurrentTime;
@@ -16440,7 +16462,7 @@ class AudioStreamController extends BaseStreamController {
16440
16462
  this.flushAudioIfNeeded(data);
16441
16463
  if (this.state !== State.STOPPED) {
16442
16464
  // switching to audio track, start timer if not already started
16443
- this.setInterval(TICK_INTERVAL$2);
16465
+ this.setInterval(TICK_INTERVAL$3);
16444
16466
  this.state = State.IDLE;
16445
16467
  this.tick();
16446
16468
  }
@@ -18151,7 +18173,6 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => key === 'in
18151
18173
  const sbTrack = transferredTrack != null && transferredTrack.buffer ? transferredTrack : track;
18152
18174
  const sbCodec = (sbTrack == null ? undefined : sbTrack.pendingCodec) || (sbTrack == null ? undefined : sbTrack.codec);
18153
18175
  const trackLevelCodec = sbTrack == null ? undefined : sbTrack.levelCodec;
18154
- const forceChangeType = !sbTrack || !!this.hls.config.assetPlayerId;
18155
18176
  if (!track) {
18156
18177
  track = tracks[trackName] = {
18157
18178
  buffer: undefined,
@@ -18168,7 +18189,7 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => key === 'in
18168
18189
  const currentCodec = currentCodecFull == null ? undefined : currentCodecFull.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1');
18169
18190
  let trackCodec = pickMostCompleteCodecName(codec, levelCodec);
18170
18191
  const nextCodec = (_trackCodec = trackCodec) == null ? undefined : _trackCodec.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1');
18171
- if (trackCodec && (currentCodec !== nextCodec || forceChangeType)) {
18192
+ if (trackCodec && currentCodecFull && currentCodec !== nextCodec) {
18172
18193
  if (trackName.slice(0, 5) === 'audio') {
18173
18194
  trackCodec = getCodecCompatibleName(trackCodec, this.appendSource);
18174
18195
  }
@@ -23623,6 +23644,14 @@ class AssetListLoader {
23623
23644
  }
23624
23645
  }
23625
23646
 
23647
+ function addEventListener(el, type, listener) {
23648
+ removeEventListener(el, type, listener);
23649
+ el.addEventListener(type, listener);
23650
+ }
23651
+ function removeEventListener(el, type, listener) {
23652
+ el.removeEventListener(type, listener);
23653
+ }
23654
+
23626
23655
  function playWithCatch(media) {
23627
23656
  media == null ? undefined : media.play().catch(() => {
23628
23657
  /* no-op */
@@ -23925,24 +23954,23 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))}`);
23925
23954
  this.onScheduleUpdate = null;
23926
23955
  }
23927
23956
  onDestroying() {
23928
- const media = this.primaryMedia;
23957
+ const media = this.primaryMedia || this.media;
23929
23958
  if (media) {
23930
23959
  this.removeMediaListeners(media);
23931
23960
  }
23932
23961
  }
23933
23962
  removeMediaListeners(media) {
23934
- media.removeEventListener('play', this.onPlay);
23935
- media.removeEventListener('pause', this.onPause);
23936
- media.removeEventListener('seeking', this.onSeeking);
23937
- media.removeEventListener('timeupdate', this.onTimeupdate);
23963
+ removeEventListener(media, 'play', this.onPlay);
23964
+ removeEventListener(media, 'pause', this.onPause);
23965
+ removeEventListener(media, 'seeking', this.onSeeking);
23966
+ removeEventListener(media, 'timeupdate', this.onTimeupdate);
23938
23967
  }
23939
23968
  onMediaAttaching(event, data) {
23940
23969
  const media = this.media = data.media;
23941
- this.removeMediaListeners(media);
23942
- media.addEventListener('seeking', this.onSeeking);
23943
- media.addEventListener('timeupdate', this.onTimeupdate);
23944
- media.addEventListener('play', this.onPlay);
23945
- media.addEventListener('pause', this.onPause);
23970
+ addEventListener(media, 'seeking', this.onSeeking);
23971
+ addEventListener(media, 'timeupdate', this.onTimeupdate);
23972
+ addEventListener(media, 'play', this.onPlay);
23973
+ addEventListener(media, 'pause', this.onPause);
23946
23974
  }
23947
23975
  onMediaAttached(event, data) {
23948
23976
  const playingItem = this.playingItem;
@@ -25020,7 +25048,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`);
25020
25048
  const primary = this.hls;
25021
25049
  const userConfig = primary.userConfig;
25022
25050
  let videoPreference = userConfig.videoPreference;
25023
- const currentLevel = primary.levels[primary.loadLevel] || primary.levels[primary.currentLevel];
25051
+ const currentLevel = primary.loadLevelObj || primary.levels[primary.currentLevel];
25024
25052
  if (videoPreference || currentLevel) {
25025
25053
  videoPreference = _extends({}, videoPreference);
25026
25054
  if (currentLevel.videoCodec) {
@@ -25405,7 +25433,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`);
25405
25433
  }
25406
25434
  }
25407
25435
 
25408
- const TICK_INTERVAL$1 = 500; // how often to tick in ms
25436
+ const TICK_INTERVAL$2 = 500; // how often to tick in ms
25409
25437
 
25410
25438
  class SubtitleStreamController extends BaseStreamController {
25411
25439
  constructor(hls, fragmentTracker, keyLoader) {
@@ -25447,7 +25475,7 @@ class SubtitleStreamController extends BaseStreamController {
25447
25475
  startLoad(startPosition) {
25448
25476
  this.stopLoad();
25449
25477
  this.state = State.IDLE;
25450
- this.setInterval(TICK_INTERVAL$1);
25478
+ this.setInterval(TICK_INTERVAL$2);
25451
25479
  this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition;
25452
25480
  this.tick();
25453
25481
  }
@@ -25583,7 +25611,7 @@ class SubtitleStreamController extends BaseStreamController {
25583
25611
  this.mediaBuffer = null;
25584
25612
  }
25585
25613
  if (currentTrack && this.state !== State.STOPPED) {
25586
- this.setInterval(TICK_INTERVAL$1);
25614
+ this.setInterval(TICK_INTERVAL$2);
25587
25615
  }
25588
25616
  }
25589
25617
 
@@ -29333,16 +29361,20 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
29333
29361
  frontBufferFlushThreshold: Infinity,
29334
29362
  maxBufferSize: 60 * 1000 * 1000,
29335
29363
  // used by stream-controller
29336
- maxBufferHole: 0.1,
29364
+ maxFragLookUpTolerance: 0.25,
29337
29365
  // used by stream-controller
29366
+ maxBufferHole: 0.1,
29367
+ // used by stream-controller and gap-controller
29368
+ detectStallWithCurrentTimeMs: 1250,
29369
+ // used by gap-controller
29338
29370
  highBufferWatchdogPeriod: 2,
29339
- // used by stream-controller
29371
+ // used by gap-controller
29340
29372
  nudgeOffset: 0.1,
29341
- // used by stream-controller
29373
+ // used by gap-controller
29342
29374
  nudgeMaxRetry: 3,
29343
- // used by stream-controller
29344
- maxFragLookUpTolerance: 0.25,
29345
- // used by stream-controller
29375
+ // used by gap-controller
29376
+ nudgeOnVideoHole: true,
29377
+ // used by gap-controller
29346
29378
  liveSyncDurationCount: 3,
29347
29379
  // used by latency-controller
29348
29380
  liveSyncOnStallIncrease: 1,
@@ -29441,7 +29473,6 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
29441
29473
  progressive: false,
29442
29474
  lowLatencyMode: true,
29443
29475
  cmcd: undefined,
29444
- detectStallWithCurrentTimeMs: 1250,
29445
29476
  enableDateRangeMetadataCues: true,
29446
29477
  enableEmsgMetadataCues: true,
29447
29478
  enableEmsgKLVMetadata: false,
@@ -29696,6 +29727,556 @@ function enableStreamingMode(config, logger) {
29696
29727
  }
29697
29728
  }
29698
29729
 
29730
+ const MAX_START_GAP_JUMP = 2.0;
29731
+ const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
29732
+ const SKIP_BUFFER_RANGE_START = 0.05;
29733
+ const TICK_INTERVAL$1 = 100;
29734
+ class GapController extends TaskLoop {
29735
+ constructor(hls, fragmentTracker) {
29736
+ super('gap-controller', hls.logger);
29737
+ this.hls = null;
29738
+ this.fragmentTracker = null;
29739
+ this.media = null;
29740
+ this.mediaSource = undefined;
29741
+ this.nudgeRetry = 0;
29742
+ this.stallReported = false;
29743
+ this.stalled = null;
29744
+ this.moved = false;
29745
+ this.seeking = false;
29746
+ this.buffered = {};
29747
+ this.lastCurrentTime = 0;
29748
+ this.ended = 0;
29749
+ this.waiting = 0;
29750
+ this.onMediaPlaying = () => {
29751
+ this.ended = 0;
29752
+ this.waiting = 0;
29753
+ };
29754
+ this.onMediaWaiting = () => {
29755
+ var _this$media;
29756
+ if ((_this$media = this.media) != null && _this$media.seeking) {
29757
+ return;
29758
+ }
29759
+ this.waiting = self.performance.now();
29760
+ this.tick();
29761
+ };
29762
+ this.onMediaEnded = () => {
29763
+ if (this.hls) {
29764
+ var _this$media2;
29765
+ // ended is set when triggering MEDIA_ENDED so that we do not trigger it again on stall or on tick with media.ended
29766
+ this.ended = ((_this$media2 = this.media) == null ? undefined : _this$media2.currentTime) || 1;
29767
+ this.hls.trigger(Events.MEDIA_ENDED, {
29768
+ stalled: false
29769
+ });
29770
+ }
29771
+ };
29772
+ this.hls = hls;
29773
+ this.fragmentTracker = fragmentTracker;
29774
+ this.registerListeners();
29775
+ }
29776
+ registerListeners() {
29777
+ const {
29778
+ hls
29779
+ } = this;
29780
+ if (hls) {
29781
+ hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
29782
+ hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
29783
+ hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
29784
+ }
29785
+ }
29786
+ unregisterListeners() {
29787
+ const {
29788
+ hls
29789
+ } = this;
29790
+ if (hls) {
29791
+ hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
29792
+ hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
29793
+ hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
29794
+ }
29795
+ }
29796
+ destroy() {
29797
+ super.destroy();
29798
+ this.unregisterListeners();
29799
+ this.media = this.hls = this.fragmentTracker = null;
29800
+ this.mediaSource = undefined;
29801
+ }
29802
+ onMediaAttached(event, data) {
29803
+ this.setInterval(TICK_INTERVAL$1);
29804
+ this.mediaSource = data.mediaSource;
29805
+ const media = this.media = data.media;
29806
+ addEventListener(media, 'playing', this.onMediaPlaying);
29807
+ addEventListener(media, 'waiting', this.onMediaWaiting);
29808
+ addEventListener(media, 'ended', this.onMediaEnded);
29809
+ }
29810
+ onMediaDetaching(event, data) {
29811
+ this.clearInterval();
29812
+ const {
29813
+ media
29814
+ } = this;
29815
+ if (media) {
29816
+ removeEventListener(media, 'playing', this.onMediaPlaying);
29817
+ removeEventListener(media, 'waiting', this.onMediaWaiting);
29818
+ removeEventListener(media, 'ended', this.onMediaEnded);
29819
+ this.media = null;
29820
+ }
29821
+ this.mediaSource = undefined;
29822
+ }
29823
+ onBufferAppended(event, data) {
29824
+ this.buffered = data.timeRanges;
29825
+ }
29826
+ get hasBuffered() {
29827
+ return Object.keys(this.buffered).length > 0;
29828
+ }
29829
+ tick() {
29830
+ var _this$media3;
29831
+ if (!((_this$media3 = this.media) != null && _this$media3.readyState) || !this.hasBuffered) {
29832
+ return;
29833
+ }
29834
+ const currentTime = this.media.currentTime;
29835
+ this.poll(currentTime, this.lastCurrentTime);
29836
+ this.lastCurrentTime = currentTime;
29837
+ }
29838
+
29839
+ /**
29840
+ * Checks if the playhead is stuck within a gap, and if so, attempts to free it.
29841
+ * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range).
29842
+ *
29843
+ * @param lastCurrentTime - Previously read playhead position
29844
+ */
29845
+ poll(currentTime, lastCurrentTime) {
29846
+ var _this$hls, _this$hls2;
29847
+ const config = (_this$hls = this.hls) == null ? undefined : _this$hls.config;
29848
+ if (!config) {
29849
+ return;
29850
+ }
29851
+ const {
29852
+ media,
29853
+ stalled
29854
+ } = this;
29855
+ if (!media) {
29856
+ return;
29857
+ }
29858
+ const {
29859
+ seeking
29860
+ } = media;
29861
+ const seeked = this.seeking && !seeking;
29862
+ const beginSeek = !this.seeking && seeking;
29863
+ const pausedEndedOrHalted = media.paused && !seeking || media.ended || media.playbackRate === 0;
29864
+ this.seeking = seeking;
29865
+
29866
+ // The playhead is moving, no-op
29867
+ if (currentTime !== lastCurrentTime) {
29868
+ if (lastCurrentTime) {
29869
+ this.ended = 0;
29870
+ }
29871
+ this.moved = true;
29872
+ if (!seeking) {
29873
+ this.nudgeRetry = 0;
29874
+ // When crossing between buffered video time ranges, but not audio, flush pipeline with seek (Chrome)
29875
+ if (config.nudgeOnVideoHole && !pausedEndedOrHalted && currentTime > lastCurrentTime) {
29876
+ this.nudgeOnVideoHole(currentTime, lastCurrentTime);
29877
+ }
29878
+ }
29879
+ if (this.waiting === 0) {
29880
+ this.stallResolved(currentTime);
29881
+ }
29882
+ return;
29883
+ }
29884
+
29885
+ // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek
29886
+ if (beginSeek || seeked) {
29887
+ if (seeked) {
29888
+ this.stallResolved(currentTime);
29889
+ }
29890
+ return;
29891
+ }
29892
+
29893
+ // The playhead should not be moving
29894
+ if (pausedEndedOrHalted) {
29895
+ this.nudgeRetry = 0;
29896
+ this.stallResolved(currentTime);
29897
+ // Fire MEDIA_ENDED to workaround event not being dispatched by browser
29898
+ if (!this.ended && media.ended && this.hls) {
29899
+ this.ended = currentTime || 1;
29900
+ this.hls.trigger(Events.MEDIA_ENDED, {
29901
+ stalled: false
29902
+ });
29903
+ }
29904
+ return;
29905
+ }
29906
+ if (!BufferHelper.getBuffered(media).length) {
29907
+ this.nudgeRetry = 0;
29908
+ return;
29909
+ }
29910
+
29911
+ // Resolve stalls at buffer holes using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
29912
+ const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
29913
+ const nextStart = bufferInfo.nextStart || 0;
29914
+ const fragmentTracker = this.fragmentTracker;
29915
+ if (seeking && fragmentTracker && this.hls) {
29916
+ // Is there a fragment loading/parsing/appending before currentTime?
29917
+ const inFlightDependency = getInFlightDependency(this.hls.inFlightFragments, currentTime);
29918
+
29919
+ // Waiting for seeking in a buffered range to complete
29920
+ const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
29921
+ // Next buffered range is too far ahead to jump to while still seeking
29922
+ const noBufferHole = !nextStart || inFlightDependency || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime);
29923
+ if (hasEnoughBuffer || noBufferHole) {
29924
+ return;
29925
+ }
29926
+ // Reset moved state when seeking to a point in or before a gap/hole
29927
+ this.moved = false;
29928
+ }
29929
+
29930
+ // Skip start gaps if we haven't played, but the last poll detected the start of a stall
29931
+ // The addition poll gives the browser a chance to jump the gap for us
29932
+ const levelDetails = (_this$hls2 = this.hls) == null ? undefined : _this$hls2.latestLevelDetails;
29933
+ if (!this.moved && this.stalled !== null && fragmentTracker) {
29934
+ // There is no playable buffer (seeked, waiting for buffer)
29935
+ const isBuffered = bufferInfo.len > 0;
29936
+ if (!isBuffered && !nextStart) {
29937
+ return;
29938
+ }
29939
+ // Jump start gaps within jump threshold
29940
+ const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime;
29941
+
29942
+ // When joining a live stream with audio tracks, account for live playlist window sliding by allowing
29943
+ // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
29944
+ // that begins over 1 target duration after the video start position.
29945
+ const isLive = !!(levelDetails != null && levelDetails.live);
29946
+ const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
29947
+ const partialOrGap = fragmentTracker.getPartialFragment(currentTime);
29948
+ if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
29949
+ if (!media.paused) {
29950
+ this._trySkipBufferHole(partialOrGap);
29951
+ }
29952
+ return;
29953
+ }
29954
+ }
29955
+
29956
+ // Start tracking stall time
29957
+ const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs;
29958
+ const tnow = self.performance.now();
29959
+ const tWaiting = this.waiting;
29960
+ if (stalled === null) {
29961
+ // Use time of recent "waiting" event
29962
+ if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) {
29963
+ this.stalled = tWaiting;
29964
+ } else {
29965
+ this.stalled = tnow;
29966
+ }
29967
+ return;
29968
+ }
29969
+ const stalledDuration = tnow - stalled;
29970
+ if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) {
29971
+ var _this$mediaSource;
29972
+ // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
29973
+ if (((_this$mediaSource = this.mediaSource) == null ? undefined : _this$mediaSource.readyState) === 'ended' && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? undefined : levelDetails.edge) || 0)) < 1) {
29974
+ if (this.ended) {
29975
+ return;
29976
+ }
29977
+ this.ended = currentTime || 1;
29978
+ this.hls.trigger(Events.MEDIA_ENDED, {
29979
+ stalled: true
29980
+ });
29981
+ return;
29982
+ }
29983
+ // Report stalling after trying to fix
29984
+ this._reportStall(bufferInfo);
29985
+ if (!this.media || !this.hls) {
29986
+ return;
29987
+ }
29988
+ }
29989
+ const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
29990
+ this._tryFixBufferStall(bufferedWithHoles, stalledDuration);
29991
+ }
29992
+ stallResolved(currentTime) {
29993
+ const stalled = this.stalled;
29994
+ if (stalled && this.hls) {
29995
+ this.stalled = null;
29996
+ // The playhead is now moving, but was previously stalled
29997
+ if (this.stallReported) {
29998
+ const stalledDuration = self.performance.now() - stalled;
29999
+ this.log(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`);
30000
+ this.stallReported = false;
30001
+ this.waiting = 0;
30002
+ this.hls.trigger(Events.STALL_RESOLVED, {});
30003
+ }
30004
+ }
30005
+ }
30006
+ nudgeOnVideoHole(currentTime, lastCurrentTime) {
30007
+ var _this$buffered$audio;
30008
+ // Chrome will play one second past a hole in video buffered time ranges without rendering any video from the subsequent range and then stall as long as audio is buffered:
30009
+ // https://github.com/video-dev/hls.js/issues/5631
30010
+ // https://issues.chromium.org/issues/40280613#comment10
30011
+ // Detect the potential for this situation and proactively seek to flush the video pipeline once the playhead passes the start of the video hole.
30012
+ // When there are audio and video buffers and currentTime is past the end of the first video buffered range...
30013
+ const videoSourceBuffered = this.buffered.video;
30014
+ if (this.hls && this.media && this.fragmentTracker && (_this$buffered$audio = this.buffered.audio) != null && _this$buffered$audio.length && videoSourceBuffered && videoSourceBuffered.length > 1 && currentTime > videoSourceBuffered.end(0)) {
30015
+ // and audio is buffered at the playhead
30016
+ const audioBufferInfo = BufferHelper.bufferedInfo(BufferHelper.timeRangesToArray(this.buffered.audio), currentTime, 0);
30017
+ if (audioBufferInfo.len > 1 && lastCurrentTime >= audioBufferInfo.start) {
30018
+ const videoTimes = BufferHelper.timeRangesToArray(videoSourceBuffered);
30019
+ const lastBufferedIndex = BufferHelper.bufferedInfo(videoTimes, lastCurrentTime, 0).bufferedIndex;
30020
+ // nudge when crossing into another video buffered range (hole).
30021
+ if (lastBufferedIndex > -1 && lastBufferedIndex < videoTimes.length - 1) {
30022
+ const bufferedIndex = BufferHelper.bufferedInfo(videoTimes, currentTime, 0).bufferedIndex;
30023
+ const holeStart = videoTimes[lastBufferedIndex].end;
30024
+ const holeEnd = videoTimes[lastBufferedIndex + 1].start;
30025
+ if ((bufferedIndex === -1 || bufferedIndex > lastBufferedIndex) && holeEnd - holeStart < 1 &&
30026
+ // `maxBufferHole` may be too small and setting it to 0 should not disable this feature
30027
+ currentTime - holeStart < 2) {
30028
+ const error = new Error(`nudging playhead to flush pipeline after video hole. currentTime: ${currentTime} hole: ${holeStart} -> ${holeEnd} buffered index: ${bufferedIndex}`);
30029
+ this.warn(error.message);
30030
+ // Magic number to flush the pipeline without interuption to audio playback:
30031
+ this.media.currentTime += 0.000001;
30032
+ const frag = this.fragmentTracker.getPartialFragment(currentTime) || undefined;
30033
+ const bufferInfo = BufferHelper.bufferInfo(this.media, currentTime, 0);
30034
+ this.hls.trigger(Events.ERROR, {
30035
+ type: ErrorTypes.MEDIA_ERROR,
30036
+ details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
30037
+ fatal: false,
30038
+ error,
30039
+ reason: error.message,
30040
+ frag,
30041
+ buffer: bufferInfo.len,
30042
+ bufferInfo
30043
+ });
30044
+ }
30045
+ }
30046
+ }
30047
+ }
30048
+ }
30049
+
30050
+ /**
30051
+ * Detects and attempts to fix known buffer stalling issues.
30052
+ * @param bufferInfo - The properties of the current buffer.
30053
+ * @param stalledDurationMs - The amount of time Hls.js has been stalling for.
30054
+ * @private
30055
+ */
30056
+ _tryFixBufferStall(bufferInfo, stalledDurationMs) {
30057
+ var _this$hls3;
30058
+ const {
30059
+ fragmentTracker,
30060
+ media
30061
+ } = this;
30062
+ const config = (_this$hls3 = this.hls) == null ? undefined : _this$hls3.config;
30063
+ if (!media || !fragmentTracker || !config) {
30064
+ return;
30065
+ }
30066
+ const currentTime = media.currentTime;
30067
+ const partial = fragmentTracker.getPartialFragment(currentTime);
30068
+ if (partial) {
30069
+ // Try to skip over the buffer hole caused by a partial fragment
30070
+ // This method isn't limited by the size of the gap between buffered ranges
30071
+ const targetTime = this._trySkipBufferHole(partial);
30072
+ // we return here in this case, meaning
30073
+ // the branch below only executes when we haven't seeked to a new position
30074
+ if (targetTime || !this.media) {
30075
+ return;
30076
+ }
30077
+ }
30078
+
30079
+ // if we haven't had to skip over a buffer hole of a partial fragment
30080
+ // we may just have to "nudge" the playlist as the browser decoding/rendering engine
30081
+ // needs to cross some sort of threshold covering all source-buffers content
30082
+ // to start playing properly.
30083
+ const bufferedRanges = bufferInfo.buffered;
30084
+ if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && (stalledDurationMs > config.highBufferWatchdogPeriod * 1000 || this.waiting)) {
30085
+ this.warn('Trying to nudge playhead over buffer-hole');
30086
+ // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
30087
+ // We only try to jump the hole if it's under the configured size
30088
+ this._tryNudgeBuffer(bufferInfo);
30089
+ }
30090
+ }
30091
+
30092
+ /**
30093
+ * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period.
30094
+ * @param bufferLen - The playhead distance from the end of the current buffer segment.
30095
+ * @private
30096
+ */
30097
+ _reportStall(bufferInfo) {
30098
+ const {
30099
+ hls,
30100
+ media,
30101
+ stallReported,
30102
+ stalled
30103
+ } = this;
30104
+ if (!stallReported && stalled !== null && media && hls) {
30105
+ // Report stalled error once
30106
+ this.stallReported = true;
30107
+ const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
30108
+ this.warn(error.message);
30109
+ hls.trigger(Events.ERROR, {
30110
+ type: ErrorTypes.MEDIA_ERROR,
30111
+ details: ErrorDetails.BUFFER_STALLED_ERROR,
30112
+ fatal: false,
30113
+ error,
30114
+ buffer: bufferInfo.len,
30115
+ bufferInfo,
30116
+ stalled: {
30117
+ start: stalled
30118
+ }
30119
+ });
30120
+ }
30121
+ }
30122
+
30123
+ /**
30124
+ * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments
30125
+ * @param partial - The partial fragment found at the current time (where playback is stalling).
30126
+ * @private
30127
+ */
30128
+ _trySkipBufferHole(partial) {
30129
+ var _this$hls4;
30130
+ const {
30131
+ fragmentTracker,
30132
+ media
30133
+ } = this;
30134
+ const config = (_this$hls4 = this.hls) == null ? undefined : _this$hls4.config;
30135
+ if (!media || !fragmentTracker || !config) {
30136
+ return 0;
30137
+ }
30138
+
30139
+ // Check if currentTime is between unbuffered regions of partial fragments
30140
+ const currentTime = media.currentTime;
30141
+ const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
30142
+ const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart;
30143
+ if (startTime && this.hls) {
30144
+ const bufferStarved = bufferInfo.len <= config.maxBufferHole;
30145
+ const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3;
30146
+ const gapLength = startTime - currentTime;
30147
+ if (gapLength > 0 && (bufferStarved || waiting)) {
30148
+ // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
30149
+ if (gapLength > config.maxBufferHole) {
30150
+ let startGap = false;
30151
+ if (currentTime === 0) {
30152
+ const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN);
30153
+ if (startFrag && startTime < startFrag.end) {
30154
+ startGap = true;
30155
+ }
30156
+ }
30157
+ if (!startGap) {
30158
+ const startProvisioned = partial || fragmentTracker.getAppendedFrag(currentTime, PlaylistLevelType.MAIN);
30159
+ if (startProvisioned) {
30160
+ var _this$hls$loadLevelOb;
30161
+ // Do not seek when selected variant playlist is unloaded
30162
+ if (!((_this$hls$loadLevelOb = this.hls.loadLevelObj) != null && _this$hls$loadLevelOb.details)) {
30163
+ return 0;
30164
+ }
30165
+ // Do not seek when required fragments are inflight or appending
30166
+ const inFlightDependency = getInFlightDependency(this.hls.inFlightFragments, startTime);
30167
+ if (inFlightDependency) {
30168
+ return 0;
30169
+ }
30170
+ // Do not seek if we can't walk tracked fragments to end of gap
30171
+ let moreToLoad = false;
30172
+ let pos = startProvisioned.end;
30173
+ while (pos < startTime) {
30174
+ const provisioned = fragmentTracker.getPartialFragment(pos);
30175
+ if (provisioned) {
30176
+ pos += provisioned.duration;
30177
+ } else {
30178
+ moreToLoad = true;
30179
+ break;
30180
+ }
30181
+ }
30182
+ if (moreToLoad) {
30183
+ return 0;
30184
+ }
30185
+ }
30186
+ }
30187
+ }
30188
+ const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
30189
+ this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
30190
+ this.moved = true;
30191
+ media.currentTime = targetTime;
30192
+ if (!(partial != null && partial.gap)) {
30193
+ const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`);
30194
+ this.hls.trigger(Events.ERROR, {
30195
+ type: ErrorTypes.MEDIA_ERROR,
30196
+ details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
30197
+ fatal: false,
30198
+ error,
30199
+ reason: error.message,
30200
+ frag: partial || undefined,
30201
+ buffer: bufferInfo.len,
30202
+ bufferInfo
30203
+ });
30204
+ }
30205
+ return targetTime;
30206
+ }
30207
+ }
30208
+ return 0;
30209
+ }
30210
+
30211
+ /**
30212
+ * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
30213
+ * @private
30214
+ */
30215
+ _tryNudgeBuffer(bufferInfo) {
30216
+ const {
30217
+ hls,
30218
+ media,
30219
+ nudgeRetry
30220
+ } = this;
30221
+ const config = hls == null ? undefined : hls.config;
30222
+ if (!media || !config) {
30223
+ return 0;
30224
+ }
30225
+ const currentTime = media.currentTime;
30226
+ this.nudgeRetry++;
30227
+ if (nudgeRetry < config.nudgeMaxRetry) {
30228
+ const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset;
30229
+ // playback stalled in buffered area ... let's nudge currentTime to try to overcome this
30230
+ const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`);
30231
+ this.warn(error.message);
30232
+ media.currentTime = targetTime;
30233
+ hls.trigger(Events.ERROR, {
30234
+ type: ErrorTypes.MEDIA_ERROR,
30235
+ details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
30236
+ error,
30237
+ fatal: false,
30238
+ buffer: bufferInfo.len,
30239
+ bufferInfo
30240
+ });
30241
+ } else {
30242
+ const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
30243
+ this.error(error.message);
30244
+ hls.trigger(Events.ERROR, {
30245
+ type: ErrorTypes.MEDIA_ERROR,
30246
+ details: ErrorDetails.BUFFER_STALLED_ERROR,
30247
+ error,
30248
+ fatal: true,
30249
+ buffer: bufferInfo.len,
30250
+ bufferInfo
30251
+ });
30252
+ }
30253
+ }
30254
+ }
30255
+ function getInFlightDependency(inFlightFragments, currentTime) {
30256
+ const main = inFlight(inFlightFragments.main);
30257
+ if (main && main.start <= currentTime) {
30258
+ return main;
30259
+ }
30260
+ const audio = inFlight(inFlightFragments.audio);
30261
+ if (audio && audio.start <= currentTime) {
30262
+ return audio;
30263
+ }
30264
+ return null;
30265
+ }
30266
+ function inFlight(inFlightData) {
30267
+ if (!inFlightData) {
30268
+ return null;
30269
+ }
30270
+ switch (inFlightData.state) {
30271
+ case State.IDLE:
30272
+ case State.STOPPED:
30273
+ case State.ENDED:
30274
+ case State.ERROR:
30275
+ return null;
30276
+ }
30277
+ return inFlightData.frag;
30278
+ }
30279
+
29699
30280
  const MIN_CUE_DURATION = 0.25;
29700
30281
  function getCueClass() {
29701
30282
  if (typeof self === 'undefined') return undefined;
@@ -30562,6 +31143,9 @@ class LevelController extends BasePlaylistController {
30562
31143
  }
30563
31144
  return this._levels;
30564
31145
  }
31146
+ get loadLevelObj() {
31147
+ return this.currentLevel;
31148
+ }
30565
31149
  get level() {
30566
31150
  return this.currentLevelIndex;
30567
31151
  }
@@ -30850,382 +31434,6 @@ function assignTrackIdsByGroup(tracks) {
30850
31434
  });
30851
31435
  }
30852
31436
 
30853
- const MAX_START_GAP_JUMP = 2.0;
30854
- const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
30855
- const SKIP_BUFFER_RANGE_START = 0.05;
30856
- class GapController extends Logger {
30857
- constructor(media, fragmentTracker, hls) {
30858
- super('gap-controller', hls.logger);
30859
- this.media = null;
30860
- this.fragmentTracker = null;
30861
- this.hls = null;
30862
- this.nudgeRetry = 0;
30863
- this.stallReported = false;
30864
- this.stalled = null;
30865
- this.moved = false;
30866
- this.seeking = false;
30867
- this.ended = 0;
30868
- this.waiting = 0;
30869
- this.media = media;
30870
- this.fragmentTracker = fragmentTracker;
30871
- this.hls = hls;
30872
- }
30873
- destroy() {
30874
- this.media = this.hls = this.fragmentTracker = null;
30875
- }
30876
-
30877
- /**
30878
- * Checks if the playhead is stuck within a gap, and if so, attempts to free it.
30879
- * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range).
30880
- *
30881
- * @param lastCurrentTime - Previously read playhead position
30882
- */
30883
- poll(lastCurrentTime, activeFrag, levelDetails, state) {
30884
- var _this$hls;
30885
- const {
30886
- media,
30887
- stalled
30888
- } = this;
30889
- if (!media) {
30890
- return;
30891
- }
30892
- const {
30893
- currentTime,
30894
- seeking
30895
- } = media;
30896
- const seeked = this.seeking && !seeking;
30897
- const beginSeek = !this.seeking && seeking;
30898
- this.seeking = seeking;
30899
-
30900
- // The playhead is moving, no-op
30901
- if (currentTime !== lastCurrentTime) {
30902
- if (lastCurrentTime) {
30903
- this.ended = 0;
30904
- }
30905
- this.moved = true;
30906
- if (!seeking) {
30907
- this.nudgeRetry = 0;
30908
- }
30909
- if (this.waiting === 0) {
30910
- this.stallResolved(currentTime);
30911
- }
30912
- return;
30913
- }
30914
-
30915
- // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek
30916
- if (beginSeek || seeked) {
30917
- if (seeked) {
30918
- this.stallResolved(currentTime);
30919
- }
30920
- return;
30921
- }
30922
-
30923
- // The playhead should not be moving
30924
- if (media.paused && !seeking || media.ended || media.playbackRate === 0) {
30925
- this.nudgeRetry = 0;
30926
- this.stallResolved(currentTime);
30927
- // Fire MEDIA_ENDED to workaround event not being dispatched by browser
30928
- if (!this.ended && media.ended && this.hls) {
30929
- this.ended = currentTime || 1;
30930
- this.hls.trigger(Events.MEDIA_ENDED, {
30931
- stalled: false
30932
- });
30933
- }
30934
- return;
30935
- }
30936
- if (!BufferHelper.getBuffered(media).length) {
30937
- this.nudgeRetry = 0;
30938
- return;
30939
- }
30940
- const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
30941
- const nextStart = bufferInfo.nextStart || 0;
30942
- const fragmentTracker = this.fragmentTracker;
30943
- if (seeking && fragmentTracker) {
30944
- // Waiting for seeking in a buffered range to complete
30945
- const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
30946
- // Next buffered range is too far ahead to jump to while still seeking
30947
- const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime);
30948
- if (hasEnoughBuffer || noBufferGap) {
30949
- return;
30950
- }
30951
- // Reset moved state when seeking to a point in or before a gap
30952
- this.moved = false;
30953
- }
30954
-
30955
- // Skip start gaps if we haven't played, but the last poll detected the start of a stall
30956
- // The addition poll gives the browser a chance to jump the gap for us
30957
- if (!this.moved && this.stalled !== null && fragmentTracker) {
30958
- // There is no playable buffer (seeked, waiting for buffer)
30959
- const isBuffered = bufferInfo.len > 0;
30960
- if (!isBuffered && !nextStart) {
30961
- return;
30962
- }
30963
- // Jump start gaps within jump threshold
30964
- const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime;
30965
-
30966
- // When joining a live stream with audio tracks, account for live playlist window sliding by allowing
30967
- // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
30968
- // that begins over 1 target duration after the video start position.
30969
- const isLive = !!(levelDetails != null && levelDetails.live);
30970
- const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
30971
- const partialOrGap = fragmentTracker.getPartialFragment(currentTime);
30972
- if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
30973
- if (!media.paused) {
30974
- this._trySkipBufferHole(partialOrGap);
30975
- }
30976
- return;
30977
- }
30978
- }
30979
-
30980
- // Start tracking stall time
30981
- const config = (_this$hls = this.hls) == null ? undefined : _this$hls.config;
30982
- if (!config) {
30983
- return;
30984
- }
30985
- const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs;
30986
- const tnow = self.performance.now();
30987
- const tWaiting = this.waiting;
30988
- if (stalled === null) {
30989
- // Use time of recent "waiting" event
30990
- if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) {
30991
- this.stalled = tWaiting;
30992
- } else {
30993
- this.stalled = tnow;
30994
- }
30995
- return;
30996
- }
30997
- const stalledDuration = tnow - stalled;
30998
- if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) {
30999
- // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
31000
- if (state === State.ENDED && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? undefined : levelDetails.edge) || 0)) < 1) {
31001
- if (this.ended) {
31002
- return;
31003
- }
31004
- this.ended = currentTime || 1;
31005
- this.hls.trigger(Events.MEDIA_ENDED, {
31006
- stalled: true
31007
- });
31008
- return;
31009
- }
31010
- // Report stalling after trying to fix
31011
- this._reportStall(bufferInfo);
31012
- if (!this.media || !this.hls) {
31013
- return;
31014
- }
31015
- }
31016
- const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
31017
- this._tryFixBufferStall(bufferedWithHoles, stalledDuration);
31018
- }
31019
- stallResolved(currentTime) {
31020
- const stalled = this.stalled;
31021
- if (stalled && this.hls) {
31022
- this.stalled = null;
31023
- // The playhead is now moving, but was previously stalled
31024
- if (this.stallReported) {
31025
- const stalledDuration = self.performance.now() - stalled;
31026
- this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`);
31027
- this.stallReported = false;
31028
- this.waiting = 0;
31029
- this.hls.trigger(Events.STALL_RESOLVED, {});
31030
- }
31031
- }
31032
- }
31033
-
31034
- /**
31035
- * Detects and attempts to fix known buffer stalling issues.
31036
- * @param bufferInfo - The properties of the current buffer.
31037
- * @param stalledDurationMs - The amount of time Hls.js has been stalling for.
31038
- * @private
31039
- */
31040
- _tryFixBufferStall(bufferInfo, stalledDurationMs) {
31041
- var _this$hls2;
31042
- const {
31043
- fragmentTracker,
31044
- media
31045
- } = this;
31046
- const config = (_this$hls2 = this.hls) == null ? undefined : _this$hls2.config;
31047
- if (!media || !fragmentTracker || !config) {
31048
- return;
31049
- }
31050
- const currentTime = media.currentTime;
31051
- const partial = fragmentTracker.getPartialFragment(currentTime);
31052
- if (partial) {
31053
- // Try to skip over the buffer hole caused by a partial fragment
31054
- // This method isn't limited by the size of the gap between buffered ranges
31055
- const targetTime = this._trySkipBufferHole(partial);
31056
- // we return here in this case, meaning
31057
- // the branch below only executes when we haven't seeked to a new position
31058
- if (targetTime || !this.media) {
31059
- return;
31060
- }
31061
- }
31062
-
31063
- // if we haven't had to skip over a buffer hole of a partial fragment
31064
- // we may just have to "nudge" the playlist as the browser decoding/rendering engine
31065
- // needs to cross some sort of threshold covering all source-buffers content
31066
- // to start playing properly.
31067
- const bufferedRanges = bufferInfo.buffered;
31068
- if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
31069
- this.warn('Trying to nudge playhead over buffer-hole');
31070
- // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
31071
- // We only try to jump the hole if it's under the configured size
31072
- this._tryNudgeBuffer(bufferInfo);
31073
- }
31074
- }
31075
-
31076
- /**
31077
- * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period.
31078
- * @param bufferLen - The playhead distance from the end of the current buffer segment.
31079
- * @private
31080
- */
31081
- _reportStall(bufferInfo) {
31082
- const {
31083
- hls,
31084
- media,
31085
- stallReported,
31086
- stalled
31087
- } = this;
31088
- if (!stallReported && stalled !== null && media && hls) {
31089
- // Report stalled error once
31090
- this.stallReported = true;
31091
- const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
31092
- this.warn(error.message);
31093
- hls.trigger(Events.ERROR, {
31094
- type: ErrorTypes.MEDIA_ERROR,
31095
- details: ErrorDetails.BUFFER_STALLED_ERROR,
31096
- fatal: false,
31097
- error,
31098
- buffer: bufferInfo.len,
31099
- bufferInfo,
31100
- stalled: {
31101
- start: stalled
31102
- }
31103
- });
31104
- }
31105
- }
31106
-
31107
- /**
31108
- * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments
31109
- * @param partial - The partial fragment found at the current time (where playback is stalling).
31110
- * @private
31111
- */
31112
- _trySkipBufferHole(partial) {
31113
- var _this$hls3;
31114
- const {
31115
- fragmentTracker,
31116
- media
31117
- } = this;
31118
- const config = (_this$hls3 = this.hls) == null ? undefined : _this$hls3.config;
31119
- if (!media || !fragmentTracker || !config) {
31120
- return 0;
31121
- }
31122
-
31123
- // Check if currentTime is between unbuffered regions of partial fragments
31124
- const currentTime = media.currentTime;
31125
- const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
31126
- const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart;
31127
- if (startTime) {
31128
- const bufferStarved = bufferInfo.len <= config.maxBufferHole;
31129
- const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3;
31130
- const gapLength = startTime - currentTime;
31131
- if (gapLength > 0 && (bufferStarved || waiting)) {
31132
- // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
31133
- if (gapLength > config.maxBufferHole) {
31134
- let startGap = false;
31135
- if (currentTime === 0) {
31136
- const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN);
31137
- if (startFrag && startTime < startFrag.end) {
31138
- startGap = true;
31139
- }
31140
- }
31141
- if (!startGap) {
31142
- const startProvisioned = partial || fragmentTracker.getAppendedFrag(currentTime, PlaylistLevelType.MAIN);
31143
- if (startProvisioned) {
31144
- let moreToLoad = false;
31145
- let pos = startProvisioned.end;
31146
- while (pos < startTime) {
31147
- const provisioned = fragmentTracker.getPartialFragment(pos);
31148
- if (provisioned) {
31149
- pos += provisioned.duration;
31150
- } else {
31151
- moreToLoad = true;
31152
- break;
31153
- }
31154
- }
31155
- if (moreToLoad) {
31156
- return 0;
31157
- }
31158
- }
31159
- }
31160
- }
31161
- const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
31162
- this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
31163
- this.moved = true;
31164
- media.currentTime = targetTime;
31165
- if (partial && !partial.gap && this.hls) {
31166
- const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`);
31167
- this.hls.trigger(Events.ERROR, {
31168
- type: ErrorTypes.MEDIA_ERROR,
31169
- details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
31170
- fatal: false,
31171
- error,
31172
- reason: error.message,
31173
- frag: partial,
31174
- buffer: bufferInfo.len,
31175
- bufferInfo
31176
- });
31177
- }
31178
- return targetTime;
31179
- }
31180
- }
31181
- return 0;
31182
- }
31183
-
31184
- /**
31185
- * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
31186
- * @private
31187
- */
31188
- _tryNudgeBuffer(bufferInfo) {
31189
- const {
31190
- hls,
31191
- media,
31192
- nudgeRetry
31193
- } = this;
31194
- const config = hls == null ? undefined : hls.config;
31195
- if (!media || !config) {
31196
- return 0;
31197
- }
31198
- const currentTime = media.currentTime;
31199
- this.nudgeRetry++;
31200
- if (nudgeRetry < config.nudgeMaxRetry) {
31201
- const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset;
31202
- // playback stalled in buffered area ... let's nudge currentTime to try to overcome this
31203
- const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`);
31204
- this.warn(error.message);
31205
- media.currentTime = targetTime;
31206
- hls.trigger(Events.ERROR, {
31207
- type: ErrorTypes.MEDIA_ERROR,
31208
- details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
31209
- error,
31210
- fatal: false,
31211
- buffer: bufferInfo.len,
31212
- bufferInfo
31213
- });
31214
- } else {
31215
- const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
31216
- this.error(error.message);
31217
- hls.trigger(Events.ERROR, {
31218
- type: ErrorTypes.MEDIA_ERROR,
31219
- details: ErrorDetails.BUFFER_STALLED_ERROR,
31220
- error,
31221
- fatal: true,
31222
- buffer: bufferInfo.len,
31223
- bufferInfo
31224
- });
31225
- }
31226
- }
31227
- }
31228
-
31229
31437
  function getSourceBuffer() {
31230
31438
  return self.SourceBuffer || self.WebKitSourceBuffer;
31231
31439
  }
@@ -31259,7 +31467,6 @@ class StreamController extends BaseStreamController {
31259
31467
  constructor(hls, fragmentTracker, keyLoader) {
31260
31468
  super(hls, fragmentTracker, keyLoader, 'stream-controller', PlaylistLevelType.MAIN);
31261
31469
  this.audioCodecSwap = false;
31262
- this.gapController = null;
31263
31470
  this.level = -1;
31264
31471
  this._forceStartLoad = false;
31265
31472
  this._hasEnoughToStart = false;
@@ -31271,19 +31478,8 @@ class StreamController extends BaseStreamController {
31271
31478
  this.backtrackFragment = null;
31272
31479
  this.audioCodecSwitch = false;
31273
31480
  this.videoBuffer = null;
31274
- this.onMediaWaiting = () => {
31275
- const gapController = this.gapController;
31276
- if (gapController) {
31277
- gapController.waiting = self.performance.now();
31278
- }
31279
- };
31280
31481
  this.onMediaPlaying = () => {
31281
31482
  // tick to speed up FRAG_CHANGED triggering
31282
- const gapController = this.gapController;
31283
- if (gapController) {
31284
- gapController.ended = 0;
31285
- gapController.waiting = 0;
31286
- }
31287
31483
  this.tick();
31288
31484
  };
31289
31485
  this.onMediaSeeked = () => {
@@ -31338,7 +31534,7 @@ class StreamController extends BaseStreamController {
31338
31534
  }
31339
31535
  onHandlerDestroying() {
31340
31536
  // @ts-ignore
31341
- this.onMediaPlaying = this.onMediaSeeked = this.onMediaWaiting = null;
31537
+ this.onMediaPlaying = this.onMediaSeeked = null;
31342
31538
  this.unregisterListeners();
31343
31539
  super.onHandlerDestroying();
31344
31540
  }
@@ -31433,8 +31629,11 @@ class StreamController extends BaseStreamController {
31433
31629
  this.onTickEnd();
31434
31630
  }
31435
31631
  onTickEnd() {
31632
+ var _this$media2;
31436
31633
  super.onTickEnd();
31437
- this.checkBuffer();
31634
+ if ((_this$media2 = this.media) != null && _this$media2.readyState && this.media.seeking === false) {
31635
+ this.lastCurrentTime = this.media.currentTime;
31636
+ }
31438
31637
  this.checkFragmentChanged();
31439
31638
  }
31440
31639
  doTickIdle() {
@@ -31667,29 +31866,19 @@ class StreamController extends BaseStreamController {
31667
31866
  onMediaAttached(event, data) {
31668
31867
  super.onMediaAttached(event, data);
31669
31868
  const media = data.media;
31670
- media.removeEventListener('playing', this.onMediaPlaying);
31671
- media.removeEventListener('seeked', this.onMediaSeeked);
31672
- media.removeEventListener('waiting', this.onMediaWaiting);
31673
- media.addEventListener('playing', this.onMediaPlaying);
31674
- media.addEventListener('seeked', this.onMediaSeeked);
31675
- media.addEventListener('waiting', this.onMediaWaiting);
31676
- this.gapController = new GapController(media, this.fragmentTracker, this.hls);
31869
+ addEventListener(media, 'playing', this.onMediaPlaying);
31870
+ addEventListener(media, 'seeked', this.onMediaSeeked);
31677
31871
  }
31678
31872
  onMediaDetaching(event, data) {
31679
31873
  const {
31680
31874
  media
31681
31875
  } = this;
31682
31876
  if (media) {
31683
- media.removeEventListener('playing', this.onMediaPlaying);
31684
- media.removeEventListener('seeked', this.onMediaSeeked);
31685
- media.removeEventListener('waiting', this.onMediaWaiting);
31877
+ removeEventListener(media, 'playing', this.onMediaPlaying);
31878
+ removeEventListener(media, 'seeked', this.onMediaSeeked);
31686
31879
  }
31687
31880
  this.videoBuffer = null;
31688
31881
  this.fragPlaying = null;
31689
- if (this.gapController) {
31690
- this.gapController.destroy();
31691
- this.gapController = null;
31692
- }
31693
31882
  super.onMediaDetaching(event, data);
31694
31883
  const transferringMedia = !!data.transferMedia;
31695
31884
  if (transferringMedia) {
@@ -31697,19 +31886,6 @@ class StreamController extends BaseStreamController {
31697
31886
  }
31698
31887
  this._hasEnoughToStart = false;
31699
31888
  }
31700
- triggerEnded() {
31701
- const gapController = this.gapController;
31702
- if (gapController) {
31703
- var _this$media2;
31704
- if (gapController.ended) {
31705
- return;
31706
- }
31707
- gapController.ended = ((_this$media2 = this.media) == null ? undefined : _this$media2.currentTime) || 1;
31708
- }
31709
- this.hls.trigger(Events.MEDIA_ENDED, {
31710
- stalled: false
31711
- });
31712
- }
31713
31889
  onManifestLoading() {
31714
31890
  super.onManifestLoading();
31715
31891
  // reset buffer on manifest loading
@@ -32044,26 +32220,6 @@ class StreamController extends BaseStreamController {
32044
32220
  break;
32045
32221
  }
32046
32222
  }
32047
-
32048
- // Checks the health of the buffer and attempts to resolve playback stalls.
32049
- checkBuffer() {
32050
- const {
32051
- media,
32052
- gapController
32053
- } = this;
32054
- if (!media || !gapController || !media.readyState) {
32055
- // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
32056
- return;
32057
- }
32058
- if (this._hasEnoughToStart || !BufferHelper.getBuffered(media).length) {
32059
- // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
32060
- const state = this.state;
32061
- const activeFrag = state !== State.IDLE ? this.fragCurrent : null;
32062
- const levelDetails = this.getLevelDetails();
32063
- gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state);
32064
- }
32065
- this.lastCurrentTime = media.currentTime;
32066
- }
32067
32223
  onFragLoadEmergencyAborted() {
32068
32224
  this.state = State.IDLE;
32069
32225
  // if loadedmetadata is not set, it means that we are emergency switch down on first frag
@@ -32079,8 +32235,10 @@ class StreamController extends BaseStreamController {
32079
32235
  }) {
32080
32236
  if (type !== ElementaryStreamTypes.AUDIO || !this.altAudio) {
32081
32237
  const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media;
32082
- this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
32083
- this.tick();
32238
+ if (mediaBuffer) {
32239
+ this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
32240
+ this.tick();
32241
+ }
32084
32242
  }
32085
32243
  }
32086
32244
  onLevelsUpdated(event, data) {
@@ -33460,9 +33618,12 @@ class Hls {
33460
33618
  this.latencyController = undefined;
33461
33619
  this.levelController = undefined;
33462
33620
  this.streamController = undefined;
33621
+ this.audioStreamController = undefined;
33622
+ this.subtititleStreamController = undefined;
33463
33623
  this.audioTrackController = undefined;
33464
33624
  this.subtitleTrackController = undefined;
33465
33625
  this.interstitialsController = undefined;
33626
+ this.gapController = undefined;
33466
33627
  this.emeController = undefined;
33467
33628
  this.cmcdController = undefined;
33468
33629
  this._media = null;
@@ -33502,6 +33663,7 @@ class Hls {
33502
33663
  const id3TrackController = new ID3TrackController(this);
33503
33664
  const keyLoader = new KeyLoader(this.config);
33504
33665
  const streamController = this.streamController = new StreamController(this, fragmentTracker, keyLoader);
33666
+ const gapController = this.gapController = new GapController(this, fragmentTracker);
33505
33667
 
33506
33668
  // Cap level controller uses streamController to flush the buffer
33507
33669
  capLevelController.setStreamController(streamController);
@@ -33515,17 +33677,17 @@ class Hls {
33515
33677
  networkControllers.splice(1, 0, contentSteering);
33516
33678
  }
33517
33679
  this.networkControllers = networkControllers;
33518
- const coreComponents = [abrController, bufferController, capLevelController, fpsController, id3TrackController, fragmentTracker];
33680
+ const coreComponents = [abrController, bufferController, gapController, capLevelController, fpsController, id3TrackController, fragmentTracker];
33519
33681
  this.audioTrackController = this.createController(config.audioTrackController, networkControllers);
33520
33682
  const AudioStreamControllerClass = config.audioStreamController;
33521
33683
  if (AudioStreamControllerClass) {
33522
- networkControllers.push(new AudioStreamControllerClass(this, fragmentTracker, keyLoader));
33684
+ networkControllers.push(this.audioStreamController = new AudioStreamControllerClass(this, fragmentTracker, keyLoader));
33523
33685
  }
33524
33686
  // Instantiate subtitleTrackController before SubtitleStreamController to receive level events first
33525
33687
  this.subtitleTrackController = this.createController(config.subtitleTrackController, networkControllers);
33526
33688
  const SubtitleStreamControllerClass = config.subtitleStreamController;
33527
33689
  if (SubtitleStreamControllerClass) {
33528
- networkControllers.push(new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader));
33690
+ networkControllers.push(this.subtititleStreamController = new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader));
33529
33691
  }
33530
33692
  this.createController(config.timelineController, coreComponents);
33531
33693
  keyLoader.emeController = this.emeController = this.createController(config.emeController, coreComponents);
@@ -33792,6 +33954,18 @@ class Hls {
33792
33954
  });
33793
33955
  }
33794
33956
  }
33957
+ get inFlightFragments() {
33958
+ const inFlightData = {
33959
+ [PlaylistLevelType.MAIN]: this.streamController.inFlightFrag
33960
+ };
33961
+ if (this.audioStreamController) {
33962
+ inFlightData[PlaylistLevelType.AUDIO] = this.audioStreamController.inFlightFrag;
33963
+ }
33964
+ if (this.subtititleStreamController) {
33965
+ inFlightData[PlaylistLevelType.SUBTITLE] = this.subtititleStreamController.inFlightFrag;
33966
+ }
33967
+ return inFlightData;
33968
+ }
33795
33969
 
33796
33970
  /**
33797
33971
  * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)
@@ -33841,10 +34015,21 @@ class Hls {
33841
34015
  const levels = this.levelController.levels;
33842
34016
  return levels ? levels : [];
33843
34017
  }
34018
+
34019
+ /**
34020
+ * @returns LevelDetails of last loaded level (variant) or `null` prior to loading a media playlist.
34021
+ */
33844
34022
  get latestLevelDetails() {
33845
34023
  return this.streamController.getLevelDetails() || null;
33846
34024
  }
33847
34025
 
34026
+ /**
34027
+ * @returns Level object of selected level (variant) or `null` prior to selecting a level or once the level is removed.
34028
+ */
34029
+ get loadLevelObj() {
34030
+ return this.levelController.loadLevelObj;
34031
+ }
34032
+
33848
34033
  /**
33849
34034
  * Index of quality level (variant) currently played
33850
34035
  */