hls.js 1.6.0-beta.2.0.canary.10923 → 1.6.0-beta.2.0.canary.10925

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.10923"}`);
405
+ newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.6.0-beta.2.0.canary.10925"}`);
406
406
  } catch (e) {
407
407
  /* log fn threw an exception. All logger methods are no-ops. */
408
408
  return createLogger();
@@ -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
  }
@@ -9838,7 +9859,7 @@ var eventemitter3 = {exports: {}};
9838
9859
  var eventemitter3Exports = eventemitter3.exports;
9839
9860
  var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports);
9840
9861
 
9841
- const version = "1.6.0-beta.2.0.canary.10923";
9862
+ const version = "1.6.0-beta.2.0.canary.10925";
9842
9863
 
9843
9864
  // ensure the worker ends up in the bundle
9844
9865
  // If the worker should not be included this gets aliased to empty.js
@@ -16085,7 +16106,7 @@ class TransmuxerInterface {
16085
16106
  }
16086
16107
  }
16087
16108
 
16088
- const TICK_INTERVAL$2 = 100; // how often to tick in ms
16109
+ const TICK_INTERVAL$3 = 100; // how often to tick in ms
16089
16110
 
16090
16111
  class AudioStreamController extends BaseStreamController {
16091
16112
  constructor(hls, fragmentTracker, keyLoader) {
@@ -16197,7 +16218,7 @@ class AudioStreamController extends BaseStreamController {
16197
16218
  }
16198
16219
  const lastCurrentTime = this.lastCurrentTime;
16199
16220
  this.stopLoad();
16200
- this.setInterval(TICK_INTERVAL$2);
16221
+ this.setInterval(TICK_INTERVAL$3);
16201
16222
  if (lastCurrentTime > 0 && startPosition === -1) {
16202
16223
  this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`);
16203
16224
  startPosition = lastCurrentTime;
@@ -16440,7 +16461,7 @@ class AudioStreamController extends BaseStreamController {
16440
16461
  this.flushAudioIfNeeded(data);
16441
16462
  if (this.state !== State.STOPPED) {
16442
16463
  // switching to audio track, start timer if not already started
16443
- this.setInterval(TICK_INTERVAL$2);
16464
+ this.setInterval(TICK_INTERVAL$3);
16444
16465
  this.state = State.IDLE;
16445
16466
  this.tick();
16446
16467
  }
@@ -18151,7 +18172,6 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => key === 'in
18151
18172
  const sbTrack = transferredTrack != null && transferredTrack.buffer ? transferredTrack : track;
18152
18173
  const sbCodec = (sbTrack == null ? undefined : sbTrack.pendingCodec) || (sbTrack == null ? undefined : sbTrack.codec);
18153
18174
  const trackLevelCodec = sbTrack == null ? undefined : sbTrack.levelCodec;
18154
- const forceChangeType = !sbTrack || !!this.hls.config.assetPlayerId;
18155
18175
  if (!track) {
18156
18176
  track = tracks[trackName] = {
18157
18177
  buffer: undefined,
@@ -18168,7 +18188,7 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => key === 'in
18168
18188
  const currentCodec = currentCodecFull == null ? undefined : currentCodecFull.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1');
18169
18189
  let trackCodec = pickMostCompleteCodecName(codec, levelCodec);
18170
18190
  const nextCodec = (_trackCodec = trackCodec) == null ? undefined : _trackCodec.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1');
18171
- if (trackCodec && (currentCodec !== nextCodec || forceChangeType)) {
18191
+ if (trackCodec && currentCodecFull && currentCodec !== nextCodec) {
18172
18192
  if (trackName.slice(0, 5) === 'audio') {
18173
18193
  trackCodec = getCodecCompatibleName(trackCodec, this.appendSource);
18174
18194
  }
@@ -21226,8 +21246,10 @@ class EMEController extends Logger {
21226
21246
  this.registerListeners();
21227
21247
  }
21228
21248
  destroy() {
21249
+ const media = this.media;
21229
21250
  this.unregisterListeners();
21230
21251
  this.onMediaDetached();
21252
+ this._clear(media);
21231
21253
  // Remove any references that could be held in config options or callbacks
21232
21254
  const config = this.config;
21233
21255
  config.requestMediaKeySystemAccessFunc = null;
@@ -21881,14 +21903,16 @@ class EMEController extends Logger {
21881
21903
  media.addEventListener('waitingforkey', this.onWaitingForKey);
21882
21904
  }
21883
21905
  onMediaDetached() {
21884
- var _media$setMediaKeys;
21885
21906
  const media = this.media;
21886
- const mediaKeysList = this.mediaKeySessions;
21887
21907
  if (media) {
21888
21908
  media.removeEventListener('encrypted', this.onMediaEncrypted);
21889
21909
  media.removeEventListener('waitingforkey', this.onWaitingForKey);
21890
21910
  this.media = null;
21891
21911
  }
21912
+ }
21913
+ _clear(media) {
21914
+ var _media$setMediaKeys;
21915
+ const mediaKeysList = this.mediaKeySessions;
21892
21916
  this._requestLicenseFailureCount = 0;
21893
21917
  this.setMediaKeysQueue = [];
21894
21918
  this.mediaKeySessions = [];
@@ -23619,6 +23643,14 @@ class AssetListLoader {
23619
23643
  }
23620
23644
  }
23621
23645
 
23646
+ function addEventListener(el, type, listener) {
23647
+ removeEventListener(el, type, listener);
23648
+ el.addEventListener(type, listener);
23649
+ }
23650
+ function removeEventListener(el, type, listener) {
23651
+ el.removeEventListener(type, listener);
23652
+ }
23653
+
23622
23654
  function playWithCatch(media) {
23623
23655
  media == null ? undefined : media.play().catch(() => {
23624
23656
  /* no-op */
@@ -23921,24 +23953,23 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))}`);
23921
23953
  this.onScheduleUpdate = null;
23922
23954
  }
23923
23955
  onDestroying() {
23924
- const media = this.primaryMedia;
23956
+ const media = this.primaryMedia || this.media;
23925
23957
  if (media) {
23926
23958
  this.removeMediaListeners(media);
23927
23959
  }
23928
23960
  }
23929
23961
  removeMediaListeners(media) {
23930
- media.removeEventListener('play', this.onPlay);
23931
- media.removeEventListener('pause', this.onPause);
23932
- media.removeEventListener('seeking', this.onSeeking);
23933
- media.removeEventListener('timeupdate', this.onTimeupdate);
23962
+ removeEventListener(media, 'play', this.onPlay);
23963
+ removeEventListener(media, 'pause', this.onPause);
23964
+ removeEventListener(media, 'seeking', this.onSeeking);
23965
+ removeEventListener(media, 'timeupdate', this.onTimeupdate);
23934
23966
  }
23935
23967
  onMediaAttaching(event, data) {
23936
23968
  const media = this.media = data.media;
23937
- this.removeMediaListeners(media);
23938
- media.addEventListener('seeking', this.onSeeking);
23939
- media.addEventListener('timeupdate', this.onTimeupdate);
23940
- media.addEventListener('play', this.onPlay);
23941
- media.addEventListener('pause', this.onPause);
23969
+ addEventListener(media, 'seeking', this.onSeeking);
23970
+ addEventListener(media, 'timeupdate', this.onTimeupdate);
23971
+ addEventListener(media, 'play', this.onPlay);
23972
+ addEventListener(media, 'pause', this.onPause);
23942
23973
  }
23943
23974
  onMediaAttached(event, data) {
23944
23975
  const playingItem = this.playingItem;
@@ -25401,7 +25432,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`);
25401
25432
  }
25402
25433
  }
25403
25434
 
25404
- const TICK_INTERVAL$1 = 500; // how often to tick in ms
25435
+ const TICK_INTERVAL$2 = 500; // how often to tick in ms
25405
25436
 
25406
25437
  class SubtitleStreamController extends BaseStreamController {
25407
25438
  constructor(hls, fragmentTracker, keyLoader) {
@@ -25443,7 +25474,7 @@ class SubtitleStreamController extends BaseStreamController {
25443
25474
  startLoad(startPosition) {
25444
25475
  this.stopLoad();
25445
25476
  this.state = State.IDLE;
25446
- this.setInterval(TICK_INTERVAL$1);
25477
+ this.setInterval(TICK_INTERVAL$2);
25447
25478
  this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition;
25448
25479
  this.tick();
25449
25480
  }
@@ -25579,7 +25610,7 @@ class SubtitleStreamController extends BaseStreamController {
25579
25610
  this.mediaBuffer = null;
25580
25611
  }
25581
25612
  if (currentTrack && this.state !== State.STOPPED) {
25582
- this.setInterval(TICK_INTERVAL$1);
25613
+ this.setInterval(TICK_INTERVAL$2);
25583
25614
  }
25584
25615
  }
25585
25616
 
@@ -29329,16 +29360,20 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
29329
29360
  frontBufferFlushThreshold: Infinity,
29330
29361
  maxBufferSize: 60 * 1000 * 1000,
29331
29362
  // used by stream-controller
29332
- maxBufferHole: 0.1,
29363
+ maxFragLookUpTolerance: 0.25,
29333
29364
  // used by stream-controller
29365
+ maxBufferHole: 0.1,
29366
+ // used by stream-controller and gap-controller
29367
+ detectStallWithCurrentTimeMs: 1250,
29368
+ // used by gap-controller
29334
29369
  highBufferWatchdogPeriod: 2,
29335
- // used by stream-controller
29370
+ // used by gap-controller
29336
29371
  nudgeOffset: 0.1,
29337
- // used by stream-controller
29372
+ // used by gap-controller
29338
29373
  nudgeMaxRetry: 3,
29339
- // used by stream-controller
29340
- maxFragLookUpTolerance: 0.25,
29341
- // used by stream-controller
29374
+ // used by gap-controller
29375
+ nudgeOnVideoHole: true,
29376
+ // used by gap-controller
29342
29377
  liveSyncDurationCount: 3,
29343
29378
  // used by latency-controller
29344
29379
  liveSyncOnStallIncrease: 1,
@@ -29437,7 +29472,6 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
29437
29472
  progressive: false,
29438
29473
  lowLatencyMode: true,
29439
29474
  cmcd: undefined,
29440
- detectStallWithCurrentTimeMs: 1250,
29441
29475
  enableDateRangeMetadataCues: true,
29442
29476
  enableEmsgMetadataCues: true,
29443
29477
  enableEmsgKLVMetadata: false,
@@ -29692,6 +29726,545 @@ function enableStreamingMode(config, logger) {
29692
29726
  }
29693
29727
  }
29694
29728
 
29729
+ const MAX_START_GAP_JUMP = 2.0;
29730
+ const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
29731
+ const SKIP_BUFFER_RANGE_START = 0.05;
29732
+ const TICK_INTERVAL$1 = 100;
29733
+ class GapController extends TaskLoop {
29734
+ constructor(hls, fragmentTracker) {
29735
+ super('gap-controller', hls.logger);
29736
+ this.hls = null;
29737
+ this.fragmentTracker = null;
29738
+ this.media = null;
29739
+ this.mediaSource = undefined;
29740
+ this.nudgeRetry = 0;
29741
+ this.stallReported = false;
29742
+ this.stalled = null;
29743
+ this.moved = false;
29744
+ this.seeking = false;
29745
+ this.buffered = {};
29746
+ this.lastCurrentTime = 0;
29747
+ this.ended = 0;
29748
+ this.waiting = 0;
29749
+ this.onMediaPlaying = () => {
29750
+ this.ended = 0;
29751
+ this.waiting = 0;
29752
+ };
29753
+ this.onMediaWaiting = () => {
29754
+ var _this$media;
29755
+ if ((_this$media = this.media) != null && _this$media.seeking) {
29756
+ return;
29757
+ }
29758
+ this.waiting = self.performance.now();
29759
+ this.tick();
29760
+ };
29761
+ this.onMediaEnded = () => {
29762
+ if (this.hls) {
29763
+ var _this$media2;
29764
+ // ended is set when triggering MEDIA_ENDED so that we do not trigger it again on stall or on tick with media.ended
29765
+ this.ended = ((_this$media2 = this.media) == null ? undefined : _this$media2.currentTime) || 1;
29766
+ this.hls.trigger(Events.MEDIA_ENDED, {
29767
+ stalled: false
29768
+ });
29769
+ }
29770
+ };
29771
+ this.hls = hls;
29772
+ this.fragmentTracker = fragmentTracker;
29773
+ this.registerListeners();
29774
+ }
29775
+ registerListeners() {
29776
+ const {
29777
+ hls
29778
+ } = this;
29779
+ if (hls) {
29780
+ hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
29781
+ hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
29782
+ hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
29783
+ }
29784
+ }
29785
+ unregisterListeners() {
29786
+ const {
29787
+ hls
29788
+ } = this;
29789
+ if (hls) {
29790
+ hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
29791
+ hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
29792
+ hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
29793
+ }
29794
+ }
29795
+ destroy() {
29796
+ super.destroy();
29797
+ this.unregisterListeners();
29798
+ this.media = this.hls = this.fragmentTracker = null;
29799
+ this.mediaSource = undefined;
29800
+ }
29801
+ onMediaAttached(event, data) {
29802
+ this.setInterval(TICK_INTERVAL$1);
29803
+ this.mediaSource = data.mediaSource;
29804
+ const media = this.media = data.media;
29805
+ addEventListener(media, 'playing', this.onMediaPlaying);
29806
+ addEventListener(media, 'waiting', this.onMediaWaiting);
29807
+ addEventListener(media, 'ended', this.onMediaEnded);
29808
+ }
29809
+ onMediaDetaching(event, data) {
29810
+ this.clearInterval();
29811
+ const {
29812
+ media
29813
+ } = this;
29814
+ if (media) {
29815
+ removeEventListener(media, 'playing', this.onMediaPlaying);
29816
+ removeEventListener(media, 'waiting', this.onMediaWaiting);
29817
+ removeEventListener(media, 'ended', this.onMediaEnded);
29818
+ this.media = null;
29819
+ }
29820
+ this.mediaSource = undefined;
29821
+ }
29822
+ onBufferAppended(event, data) {
29823
+ this.buffered = data.timeRanges;
29824
+ }
29825
+ get hasBuffered() {
29826
+ return Object.keys(this.buffered).length > 0;
29827
+ }
29828
+ tick() {
29829
+ var _this$media3;
29830
+ if (!((_this$media3 = this.media) != null && _this$media3.readyState) || !this.hasBuffered) {
29831
+ return;
29832
+ }
29833
+ const currentTime = this.media.currentTime;
29834
+ this.poll(currentTime, this.lastCurrentTime);
29835
+ this.lastCurrentTime = currentTime;
29836
+ }
29837
+
29838
+ /**
29839
+ * Checks if the playhead is stuck within a gap, and if so, attempts to free it.
29840
+ * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range).
29841
+ *
29842
+ * @param lastCurrentTime - Previously read playhead position
29843
+ */
29844
+ poll(currentTime, lastCurrentTime) {
29845
+ var _this$hls, _this$hls2;
29846
+ const config = (_this$hls = this.hls) == null ? undefined : _this$hls.config;
29847
+ if (!config) {
29848
+ return;
29849
+ }
29850
+ const {
29851
+ media,
29852
+ stalled
29853
+ } = this;
29854
+ if (!media) {
29855
+ return;
29856
+ }
29857
+ const {
29858
+ seeking
29859
+ } = media;
29860
+ const seeked = this.seeking && !seeking;
29861
+ const beginSeek = !this.seeking && seeking;
29862
+ const pausedEndedOrHalted = media.paused && !seeking || media.ended || media.playbackRate === 0;
29863
+ this.seeking = seeking;
29864
+
29865
+ // The playhead is moving, no-op
29866
+ if (currentTime !== lastCurrentTime) {
29867
+ if (lastCurrentTime) {
29868
+ this.ended = 0;
29869
+ }
29870
+ this.moved = true;
29871
+ if (!seeking) {
29872
+ this.nudgeRetry = 0;
29873
+ // When crossing between buffered video time ranges, but not audio, flush pipeline with seek (Chrome)
29874
+ if (config.nudgeOnVideoHole && !pausedEndedOrHalted && currentTime > lastCurrentTime) {
29875
+ this.nudgeOnVideoHole(currentTime, lastCurrentTime);
29876
+ }
29877
+ }
29878
+ if (this.waiting === 0) {
29879
+ this.stallResolved(currentTime);
29880
+ }
29881
+ return;
29882
+ }
29883
+
29884
+ // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek
29885
+ if (beginSeek || seeked) {
29886
+ if (seeked) {
29887
+ this.stallResolved(currentTime);
29888
+ }
29889
+ return;
29890
+ }
29891
+
29892
+ // The playhead should not be moving
29893
+ if (pausedEndedOrHalted) {
29894
+ this.nudgeRetry = 0;
29895
+ this.stallResolved(currentTime);
29896
+ // Fire MEDIA_ENDED to workaround event not being dispatched by browser
29897
+ if (!this.ended && media.ended && this.hls) {
29898
+ this.ended = currentTime || 1;
29899
+ this.hls.trigger(Events.MEDIA_ENDED, {
29900
+ stalled: false
29901
+ });
29902
+ }
29903
+ return;
29904
+ }
29905
+ if (!BufferHelper.getBuffered(media).length) {
29906
+ this.nudgeRetry = 0;
29907
+ return;
29908
+ }
29909
+
29910
+ // Resolve stalls at buffer holes using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
29911
+ const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
29912
+ const nextStart = bufferInfo.nextStart || 0;
29913
+ const fragmentTracker = this.fragmentTracker;
29914
+ if (seeking && fragmentTracker && this.hls) {
29915
+ // Is there a fragment loading/parsing/appending before currentTime?
29916
+ const inFlightDependency = getInFlightDependency(this.hls.inFlightFragments, currentTime);
29917
+
29918
+ // Waiting for seeking in a buffered range to complete
29919
+ const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
29920
+ // Next buffered range is too far ahead to jump to while still seeking
29921
+ const noBufferHole = !nextStart || inFlightDependency || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime);
29922
+ if (hasEnoughBuffer || noBufferHole) {
29923
+ return;
29924
+ }
29925
+ // Reset moved state when seeking to a point in or before a gap/hole
29926
+ this.moved = false;
29927
+ }
29928
+
29929
+ // Skip start gaps if we haven't played, but the last poll detected the start of a stall
29930
+ // The addition poll gives the browser a chance to jump the gap for us
29931
+ const levelDetails = (_this$hls2 = this.hls) == null ? undefined : _this$hls2.latestLevelDetails;
29932
+ if (!this.moved && this.stalled !== null && fragmentTracker) {
29933
+ // There is no playable buffer (seeked, waiting for buffer)
29934
+ const isBuffered = bufferInfo.len > 0;
29935
+ if (!isBuffered && !nextStart) {
29936
+ return;
29937
+ }
29938
+ // Jump start gaps within jump threshold
29939
+ const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime;
29940
+
29941
+ // When joining a live stream with audio tracks, account for live playlist window sliding by allowing
29942
+ // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
29943
+ // that begins over 1 target duration after the video start position.
29944
+ const isLive = !!(levelDetails != null && levelDetails.live);
29945
+ const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
29946
+ const partialOrGap = fragmentTracker.getPartialFragment(currentTime);
29947
+ if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
29948
+ if (!media.paused) {
29949
+ this._trySkipBufferHole(partialOrGap);
29950
+ }
29951
+ return;
29952
+ }
29953
+ }
29954
+
29955
+ // Start tracking stall time
29956
+ const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs;
29957
+ const tnow = self.performance.now();
29958
+ const tWaiting = this.waiting;
29959
+ if (stalled === null) {
29960
+ // Use time of recent "waiting" event
29961
+ if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) {
29962
+ this.stalled = tWaiting;
29963
+ } else {
29964
+ this.stalled = tnow;
29965
+ }
29966
+ return;
29967
+ }
29968
+ const stalledDuration = tnow - stalled;
29969
+ if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) {
29970
+ var _this$mediaSource;
29971
+ // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
29972
+ 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) {
29973
+ if (this.ended) {
29974
+ return;
29975
+ }
29976
+ this.ended = currentTime || 1;
29977
+ this.hls.trigger(Events.MEDIA_ENDED, {
29978
+ stalled: true
29979
+ });
29980
+ return;
29981
+ }
29982
+ // Report stalling after trying to fix
29983
+ this._reportStall(bufferInfo);
29984
+ if (!this.media || !this.hls) {
29985
+ return;
29986
+ }
29987
+ }
29988
+ const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
29989
+ this._tryFixBufferStall(bufferedWithHoles, stalledDuration);
29990
+ }
29991
+ stallResolved(currentTime) {
29992
+ const stalled = this.stalled;
29993
+ if (stalled && this.hls) {
29994
+ this.stalled = null;
29995
+ // The playhead is now moving, but was previously stalled
29996
+ if (this.stallReported) {
29997
+ const stalledDuration = self.performance.now() - stalled;
29998
+ this.log(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`);
29999
+ this.stallReported = false;
30000
+ this.waiting = 0;
30001
+ this.hls.trigger(Events.STALL_RESOLVED, {});
30002
+ }
30003
+ }
30004
+ }
30005
+ nudgeOnVideoHole(currentTime, lastCurrentTime) {
30006
+ var _this$buffered$audio;
30007
+ // 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:
30008
+ // https://github.com/video-dev/hls.js/issues/5631
30009
+ // https://issues.chromium.org/issues/40280613#comment10
30010
+ // Detect the potential for this situation and proactively seek to flush the video pipeline once the playhead passes the start of the video hole.
30011
+ // When there are audio and video buffers and currentTime is past the end of the first video buffered range...
30012
+ const videoSourceBuffered = this.buffered.video;
30013
+ 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)) {
30014
+ // and audio is buffered at the playhead
30015
+ const audioBufferInfo = BufferHelper.bufferedInfo(BufferHelper.timeRangesToArray(this.buffered.audio), currentTime, 0);
30016
+ if (audioBufferInfo.len > 1 && lastCurrentTime >= audioBufferInfo.start) {
30017
+ const videoTimes = BufferHelper.timeRangesToArray(videoSourceBuffered);
30018
+ const lastBufferedIndex = BufferHelper.bufferedInfo(videoTimes, lastCurrentTime, 0).bufferedIndex;
30019
+ // nudge when crossing into another video buffered range (hole).
30020
+ if (lastBufferedIndex > -1 && lastBufferedIndex < videoTimes.length - 1) {
30021
+ const bufferedIndex = BufferHelper.bufferedInfo(videoTimes, currentTime, 0).bufferedIndex;
30022
+ const holeStart = videoTimes[lastBufferedIndex].end;
30023
+ const holeEnd = videoTimes[lastBufferedIndex + 1].start;
30024
+ if ((bufferedIndex === -1 || bufferedIndex > lastBufferedIndex) && holeEnd - holeStart < 1 &&
30025
+ // `maxBufferHole` may be too small and setting it to 0 should not disable this feature
30026
+ currentTime - holeStart < 2) {
30027
+ const error = new Error(`nudging playhead to flush pipeline after video hole. currentTime: ${currentTime} hole: ${holeStart} -> ${holeEnd} buffered index: ${bufferedIndex}`);
30028
+ this.warn(error.message);
30029
+ // Magic number to flush the pipeline without interuption to audio playback:
30030
+ this.media.currentTime += 0.000001;
30031
+ const frag = this.fragmentTracker.getPartialFragment(currentTime) || undefined;
30032
+ const bufferInfo = BufferHelper.bufferInfo(this.media, currentTime, 0);
30033
+ this.hls.trigger(Events.ERROR, {
30034
+ type: ErrorTypes.MEDIA_ERROR,
30035
+ details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
30036
+ fatal: false,
30037
+ error,
30038
+ reason: error.message,
30039
+ frag,
30040
+ buffer: bufferInfo.len,
30041
+ bufferInfo
30042
+ });
30043
+ }
30044
+ }
30045
+ }
30046
+ }
30047
+ }
30048
+
30049
+ /**
30050
+ * Detects and attempts to fix known buffer stalling issues.
30051
+ * @param bufferInfo - The properties of the current buffer.
30052
+ * @param stalledDurationMs - The amount of time Hls.js has been stalling for.
30053
+ * @private
30054
+ */
30055
+ _tryFixBufferStall(bufferInfo, stalledDurationMs) {
30056
+ var _this$hls3;
30057
+ const {
30058
+ fragmentTracker,
30059
+ media
30060
+ } = this;
30061
+ const config = (_this$hls3 = this.hls) == null ? undefined : _this$hls3.config;
30062
+ if (!media || !fragmentTracker || !config) {
30063
+ return;
30064
+ }
30065
+ const currentTime = media.currentTime;
30066
+ const partial = fragmentTracker.getPartialFragment(currentTime);
30067
+ if (partial) {
30068
+ // Try to skip over the buffer hole caused by a partial fragment
30069
+ // This method isn't limited by the size of the gap between buffered ranges
30070
+ const targetTime = this._trySkipBufferHole(partial);
30071
+ // we return here in this case, meaning
30072
+ // the branch below only executes when we haven't seeked to a new position
30073
+ if (targetTime || !this.media) {
30074
+ return;
30075
+ }
30076
+ }
30077
+
30078
+ // if we haven't had to skip over a buffer hole of a partial fragment
30079
+ // we may just have to "nudge" the playlist as the browser decoding/rendering engine
30080
+ // needs to cross some sort of threshold covering all source-buffers content
30081
+ // to start playing properly.
30082
+ const bufferedRanges = bufferInfo.buffered;
30083
+ if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && (stalledDurationMs > config.highBufferWatchdogPeriod * 1000 || this.waiting)) {
30084
+ this.warn('Trying to nudge playhead over buffer-hole');
30085
+ // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
30086
+ // We only try to jump the hole if it's under the configured size
30087
+ this._tryNudgeBuffer(bufferInfo);
30088
+ }
30089
+ }
30090
+
30091
+ /**
30092
+ * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period.
30093
+ * @param bufferLen - The playhead distance from the end of the current buffer segment.
30094
+ * @private
30095
+ */
30096
+ _reportStall(bufferInfo) {
30097
+ const {
30098
+ hls,
30099
+ media,
30100
+ stallReported,
30101
+ stalled
30102
+ } = this;
30103
+ if (!stallReported && stalled !== null && media && hls) {
30104
+ // Report stalled error once
30105
+ this.stallReported = true;
30106
+ const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
30107
+ this.warn(error.message);
30108
+ hls.trigger(Events.ERROR, {
30109
+ type: ErrorTypes.MEDIA_ERROR,
30110
+ details: ErrorDetails.BUFFER_STALLED_ERROR,
30111
+ fatal: false,
30112
+ error,
30113
+ buffer: bufferInfo.len,
30114
+ bufferInfo,
30115
+ stalled: {
30116
+ start: stalled
30117
+ }
30118
+ });
30119
+ }
30120
+ }
30121
+
30122
+ /**
30123
+ * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments
30124
+ * @param partial - The partial fragment found at the current time (where playback is stalling).
30125
+ * @private
30126
+ */
30127
+ _trySkipBufferHole(partial) {
30128
+ var _this$hls4;
30129
+ const {
30130
+ fragmentTracker,
30131
+ media
30132
+ } = this;
30133
+ const config = (_this$hls4 = this.hls) == null ? undefined : _this$hls4.config;
30134
+ if (!media || !fragmentTracker || !config) {
30135
+ return 0;
30136
+ }
30137
+
30138
+ // Check if currentTime is between unbuffered regions of partial fragments
30139
+ const currentTime = media.currentTime;
30140
+ const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
30141
+ const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart;
30142
+ if (startTime) {
30143
+ const bufferStarved = bufferInfo.len <= config.maxBufferHole;
30144
+ const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3;
30145
+ const gapLength = startTime - currentTime;
30146
+ if (gapLength > 0 && (bufferStarved || waiting)) {
30147
+ // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
30148
+ if (gapLength > config.maxBufferHole) {
30149
+ let startGap = false;
30150
+ if (currentTime === 0) {
30151
+ const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN);
30152
+ if (startFrag && startTime < startFrag.end) {
30153
+ startGap = true;
30154
+ }
30155
+ }
30156
+ if (!startGap) {
30157
+ const startProvisioned = partial || fragmentTracker.getAppendedFrag(currentTime, PlaylistLevelType.MAIN);
30158
+ if (startProvisioned) {
30159
+ let moreToLoad = false;
30160
+ let pos = startProvisioned.end;
30161
+ while (pos < startTime) {
30162
+ const provisioned = fragmentTracker.getPartialFragment(pos);
30163
+ if (provisioned) {
30164
+ pos += provisioned.duration;
30165
+ } else {
30166
+ moreToLoad = true;
30167
+ break;
30168
+ }
30169
+ }
30170
+ if (moreToLoad) {
30171
+ return 0;
30172
+ }
30173
+ }
30174
+ }
30175
+ }
30176
+ const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
30177
+ this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
30178
+ this.moved = true;
30179
+ media.currentTime = targetTime;
30180
+ if (!(partial != null && partial.gap) && this.hls) {
30181
+ const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`);
30182
+ this.hls.trigger(Events.ERROR, {
30183
+ type: ErrorTypes.MEDIA_ERROR,
30184
+ details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
30185
+ fatal: false,
30186
+ error,
30187
+ reason: error.message,
30188
+ frag: partial || undefined,
30189
+ buffer: bufferInfo.len,
30190
+ bufferInfo
30191
+ });
30192
+ }
30193
+ return targetTime;
30194
+ }
30195
+ }
30196
+ return 0;
30197
+ }
30198
+
30199
+ /**
30200
+ * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
30201
+ * @private
30202
+ */
30203
+ _tryNudgeBuffer(bufferInfo) {
30204
+ const {
30205
+ hls,
30206
+ media,
30207
+ nudgeRetry
30208
+ } = this;
30209
+ const config = hls == null ? undefined : hls.config;
30210
+ if (!media || !config) {
30211
+ return 0;
30212
+ }
30213
+ const currentTime = media.currentTime;
30214
+ this.nudgeRetry++;
30215
+ if (nudgeRetry < config.nudgeMaxRetry) {
30216
+ const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset;
30217
+ // playback stalled in buffered area ... let's nudge currentTime to try to overcome this
30218
+ const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`);
30219
+ this.warn(error.message);
30220
+ media.currentTime = targetTime;
30221
+ hls.trigger(Events.ERROR, {
30222
+ type: ErrorTypes.MEDIA_ERROR,
30223
+ details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
30224
+ error,
30225
+ fatal: false,
30226
+ buffer: bufferInfo.len,
30227
+ bufferInfo
30228
+ });
30229
+ } else {
30230
+ const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
30231
+ this.error(error.message);
30232
+ hls.trigger(Events.ERROR, {
30233
+ type: ErrorTypes.MEDIA_ERROR,
30234
+ details: ErrorDetails.BUFFER_STALLED_ERROR,
30235
+ error,
30236
+ fatal: true,
30237
+ buffer: bufferInfo.len,
30238
+ bufferInfo
30239
+ });
30240
+ }
30241
+ }
30242
+ }
30243
+ function getInFlightDependency(inFlightFragments, currentTime) {
30244
+ const main = inFlight(inFlightFragments.main);
30245
+ if (main && main.start <= currentTime) {
30246
+ return main;
30247
+ }
30248
+ const audio = inFlight(inFlightFragments.audio);
30249
+ if (audio && audio.start <= currentTime) {
30250
+ return audio;
30251
+ }
30252
+ return null;
30253
+ }
30254
+ function inFlight(inFlightData) {
30255
+ if (!inFlightData) {
30256
+ return null;
30257
+ }
30258
+ switch (inFlightData.state) {
30259
+ case State.IDLE:
30260
+ case State.STOPPED:
30261
+ case State.ENDED:
30262
+ case State.ERROR:
30263
+ return null;
30264
+ }
30265
+ return inFlightData.frag;
30266
+ }
30267
+
29695
30268
  const MIN_CUE_DURATION = 0.25;
29696
30269
  function getCueClass() {
29697
30270
  if (typeof self === 'undefined') return undefined;
@@ -30846,382 +31419,6 @@ function assignTrackIdsByGroup(tracks) {
30846
31419
  });
30847
31420
  }
30848
31421
 
30849
- const MAX_START_GAP_JUMP = 2.0;
30850
- const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
30851
- const SKIP_BUFFER_RANGE_START = 0.05;
30852
- class GapController extends Logger {
30853
- constructor(media, fragmentTracker, hls) {
30854
- super('gap-controller', hls.logger);
30855
- this.media = null;
30856
- this.fragmentTracker = null;
30857
- this.hls = null;
30858
- this.nudgeRetry = 0;
30859
- this.stallReported = false;
30860
- this.stalled = null;
30861
- this.moved = false;
30862
- this.seeking = false;
30863
- this.ended = 0;
30864
- this.waiting = 0;
30865
- this.media = media;
30866
- this.fragmentTracker = fragmentTracker;
30867
- this.hls = hls;
30868
- }
30869
- destroy() {
30870
- this.media = this.hls = this.fragmentTracker = null;
30871
- }
30872
-
30873
- /**
30874
- * Checks if the playhead is stuck within a gap, and if so, attempts to free it.
30875
- * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range).
30876
- *
30877
- * @param lastCurrentTime - Previously read playhead position
30878
- */
30879
- poll(lastCurrentTime, activeFrag, levelDetails, state) {
30880
- var _this$hls;
30881
- const {
30882
- media,
30883
- stalled
30884
- } = this;
30885
- if (!media) {
30886
- return;
30887
- }
30888
- const {
30889
- currentTime,
30890
- seeking
30891
- } = media;
30892
- const seeked = this.seeking && !seeking;
30893
- const beginSeek = !this.seeking && seeking;
30894
- this.seeking = seeking;
30895
-
30896
- // The playhead is moving, no-op
30897
- if (currentTime !== lastCurrentTime) {
30898
- if (lastCurrentTime) {
30899
- this.ended = 0;
30900
- }
30901
- this.moved = true;
30902
- if (!seeking) {
30903
- this.nudgeRetry = 0;
30904
- }
30905
- if (this.waiting === 0) {
30906
- this.stallResolved(currentTime);
30907
- }
30908
- return;
30909
- }
30910
-
30911
- // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek
30912
- if (beginSeek || seeked) {
30913
- if (seeked) {
30914
- this.stallResolved(currentTime);
30915
- }
30916
- return;
30917
- }
30918
-
30919
- // The playhead should not be moving
30920
- if (media.paused && !seeking || media.ended || media.playbackRate === 0) {
30921
- this.nudgeRetry = 0;
30922
- this.stallResolved(currentTime);
30923
- // Fire MEDIA_ENDED to workaround event not being dispatched by browser
30924
- if (!this.ended && media.ended && this.hls) {
30925
- this.ended = currentTime || 1;
30926
- this.hls.trigger(Events.MEDIA_ENDED, {
30927
- stalled: false
30928
- });
30929
- }
30930
- return;
30931
- }
30932
- if (!BufferHelper.getBuffered(media).length) {
30933
- this.nudgeRetry = 0;
30934
- return;
30935
- }
30936
- const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
30937
- const nextStart = bufferInfo.nextStart || 0;
30938
- const fragmentTracker = this.fragmentTracker;
30939
- if (seeking && fragmentTracker) {
30940
- // Waiting for seeking in a buffered range to complete
30941
- const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
30942
- // Next buffered range is too far ahead to jump to while still seeking
30943
- const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime);
30944
- if (hasEnoughBuffer || noBufferGap) {
30945
- return;
30946
- }
30947
- // Reset moved state when seeking to a point in or before a gap
30948
- this.moved = false;
30949
- }
30950
-
30951
- // Skip start gaps if we haven't played, but the last poll detected the start of a stall
30952
- // The addition poll gives the browser a chance to jump the gap for us
30953
- if (!this.moved && this.stalled !== null && fragmentTracker) {
30954
- // There is no playable buffer (seeked, waiting for buffer)
30955
- const isBuffered = bufferInfo.len > 0;
30956
- if (!isBuffered && !nextStart) {
30957
- return;
30958
- }
30959
- // Jump start gaps within jump threshold
30960
- const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime;
30961
-
30962
- // When joining a live stream with audio tracks, account for live playlist window sliding by allowing
30963
- // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
30964
- // that begins over 1 target duration after the video start position.
30965
- const isLive = !!(levelDetails != null && levelDetails.live);
30966
- const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
30967
- const partialOrGap = fragmentTracker.getPartialFragment(currentTime);
30968
- if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
30969
- if (!media.paused) {
30970
- this._trySkipBufferHole(partialOrGap);
30971
- }
30972
- return;
30973
- }
30974
- }
30975
-
30976
- // Start tracking stall time
30977
- const config = (_this$hls = this.hls) == null ? undefined : _this$hls.config;
30978
- if (!config) {
30979
- return;
30980
- }
30981
- const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs;
30982
- const tnow = self.performance.now();
30983
- const tWaiting = this.waiting;
30984
- if (stalled === null) {
30985
- // Use time of recent "waiting" event
30986
- if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) {
30987
- this.stalled = tWaiting;
30988
- } else {
30989
- this.stalled = tnow;
30990
- }
30991
- return;
30992
- }
30993
- const stalledDuration = tnow - stalled;
30994
- if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) {
30995
- // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
30996
- if (state === State.ENDED && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? undefined : levelDetails.edge) || 0)) < 1) {
30997
- if (this.ended) {
30998
- return;
30999
- }
31000
- this.ended = currentTime || 1;
31001
- this.hls.trigger(Events.MEDIA_ENDED, {
31002
- stalled: true
31003
- });
31004
- return;
31005
- }
31006
- // Report stalling after trying to fix
31007
- this._reportStall(bufferInfo);
31008
- if (!this.media || !this.hls) {
31009
- return;
31010
- }
31011
- }
31012
- const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
31013
- this._tryFixBufferStall(bufferedWithHoles, stalledDuration);
31014
- }
31015
- stallResolved(currentTime) {
31016
- const stalled = this.stalled;
31017
- if (stalled && this.hls) {
31018
- this.stalled = null;
31019
- // The playhead is now moving, but was previously stalled
31020
- if (this.stallReported) {
31021
- const stalledDuration = self.performance.now() - stalled;
31022
- this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`);
31023
- this.stallReported = false;
31024
- this.waiting = 0;
31025
- this.hls.trigger(Events.STALL_RESOLVED, {});
31026
- }
31027
- }
31028
- }
31029
-
31030
- /**
31031
- * Detects and attempts to fix known buffer stalling issues.
31032
- * @param bufferInfo - The properties of the current buffer.
31033
- * @param stalledDurationMs - The amount of time Hls.js has been stalling for.
31034
- * @private
31035
- */
31036
- _tryFixBufferStall(bufferInfo, stalledDurationMs) {
31037
- var _this$hls2;
31038
- const {
31039
- fragmentTracker,
31040
- media
31041
- } = this;
31042
- const config = (_this$hls2 = this.hls) == null ? undefined : _this$hls2.config;
31043
- if (!media || !fragmentTracker || !config) {
31044
- return;
31045
- }
31046
- const currentTime = media.currentTime;
31047
- const partial = fragmentTracker.getPartialFragment(currentTime);
31048
- if (partial) {
31049
- // Try to skip over the buffer hole caused by a partial fragment
31050
- // This method isn't limited by the size of the gap between buffered ranges
31051
- const targetTime = this._trySkipBufferHole(partial);
31052
- // we return here in this case, meaning
31053
- // the branch below only executes when we haven't seeked to a new position
31054
- if (targetTime || !this.media) {
31055
- return;
31056
- }
31057
- }
31058
-
31059
- // if we haven't had to skip over a buffer hole of a partial fragment
31060
- // we may just have to "nudge" the playlist as the browser decoding/rendering engine
31061
- // needs to cross some sort of threshold covering all source-buffers content
31062
- // to start playing properly.
31063
- const bufferedRanges = bufferInfo.buffered;
31064
- if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
31065
- this.warn('Trying to nudge playhead over buffer-hole');
31066
- // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
31067
- // We only try to jump the hole if it's under the configured size
31068
- this._tryNudgeBuffer(bufferInfo);
31069
- }
31070
- }
31071
-
31072
- /**
31073
- * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period.
31074
- * @param bufferLen - The playhead distance from the end of the current buffer segment.
31075
- * @private
31076
- */
31077
- _reportStall(bufferInfo) {
31078
- const {
31079
- hls,
31080
- media,
31081
- stallReported,
31082
- stalled
31083
- } = this;
31084
- if (!stallReported && stalled !== null && media && hls) {
31085
- // Report stalled error once
31086
- this.stallReported = true;
31087
- const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
31088
- this.warn(error.message);
31089
- hls.trigger(Events.ERROR, {
31090
- type: ErrorTypes.MEDIA_ERROR,
31091
- details: ErrorDetails.BUFFER_STALLED_ERROR,
31092
- fatal: false,
31093
- error,
31094
- buffer: bufferInfo.len,
31095
- bufferInfo,
31096
- stalled: {
31097
- start: stalled
31098
- }
31099
- });
31100
- }
31101
- }
31102
-
31103
- /**
31104
- * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments
31105
- * @param partial - The partial fragment found at the current time (where playback is stalling).
31106
- * @private
31107
- */
31108
- _trySkipBufferHole(partial) {
31109
- var _this$hls3;
31110
- const {
31111
- fragmentTracker,
31112
- media
31113
- } = this;
31114
- const config = (_this$hls3 = this.hls) == null ? undefined : _this$hls3.config;
31115
- if (!media || !fragmentTracker || !config) {
31116
- return 0;
31117
- }
31118
-
31119
- // Check if currentTime is between unbuffered regions of partial fragments
31120
- const currentTime = media.currentTime;
31121
- const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
31122
- const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart;
31123
- if (startTime) {
31124
- const bufferStarved = bufferInfo.len <= config.maxBufferHole;
31125
- const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3;
31126
- const gapLength = startTime - currentTime;
31127
- if (gapLength > 0 && (bufferStarved || waiting)) {
31128
- // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
31129
- if (gapLength > config.maxBufferHole) {
31130
- let startGap = false;
31131
- if (currentTime === 0) {
31132
- const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN);
31133
- if (startFrag && startTime < startFrag.end) {
31134
- startGap = true;
31135
- }
31136
- }
31137
- if (!startGap) {
31138
- const startProvisioned = partial || fragmentTracker.getAppendedFrag(currentTime, PlaylistLevelType.MAIN);
31139
- if (startProvisioned) {
31140
- let moreToLoad = false;
31141
- let pos = startProvisioned.end;
31142
- while (pos < startTime) {
31143
- const provisioned = fragmentTracker.getPartialFragment(pos);
31144
- if (provisioned) {
31145
- pos += provisioned.duration;
31146
- } else {
31147
- moreToLoad = true;
31148
- break;
31149
- }
31150
- }
31151
- if (moreToLoad) {
31152
- return 0;
31153
- }
31154
- }
31155
- }
31156
- }
31157
- const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
31158
- this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
31159
- this.moved = true;
31160
- media.currentTime = targetTime;
31161
- if (partial && !partial.gap && this.hls) {
31162
- const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`);
31163
- this.hls.trigger(Events.ERROR, {
31164
- type: ErrorTypes.MEDIA_ERROR,
31165
- details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
31166
- fatal: false,
31167
- error,
31168
- reason: error.message,
31169
- frag: partial,
31170
- buffer: bufferInfo.len,
31171
- bufferInfo
31172
- });
31173
- }
31174
- return targetTime;
31175
- }
31176
- }
31177
- return 0;
31178
- }
31179
-
31180
- /**
31181
- * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
31182
- * @private
31183
- */
31184
- _tryNudgeBuffer(bufferInfo) {
31185
- const {
31186
- hls,
31187
- media,
31188
- nudgeRetry
31189
- } = this;
31190
- const config = hls == null ? undefined : hls.config;
31191
- if (!media || !config) {
31192
- return 0;
31193
- }
31194
- const currentTime = media.currentTime;
31195
- this.nudgeRetry++;
31196
- if (nudgeRetry < config.nudgeMaxRetry) {
31197
- const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset;
31198
- // playback stalled in buffered area ... let's nudge currentTime to try to overcome this
31199
- const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`);
31200
- this.warn(error.message);
31201
- media.currentTime = targetTime;
31202
- hls.trigger(Events.ERROR, {
31203
- type: ErrorTypes.MEDIA_ERROR,
31204
- details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
31205
- error,
31206
- fatal: false,
31207
- buffer: bufferInfo.len,
31208
- bufferInfo
31209
- });
31210
- } else {
31211
- const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
31212
- this.error(error.message);
31213
- hls.trigger(Events.ERROR, {
31214
- type: ErrorTypes.MEDIA_ERROR,
31215
- details: ErrorDetails.BUFFER_STALLED_ERROR,
31216
- error,
31217
- fatal: true,
31218
- buffer: bufferInfo.len,
31219
- bufferInfo
31220
- });
31221
- }
31222
- }
31223
- }
31224
-
31225
31422
  function getSourceBuffer() {
31226
31423
  return self.SourceBuffer || self.WebKitSourceBuffer;
31227
31424
  }
@@ -31255,7 +31452,6 @@ class StreamController extends BaseStreamController {
31255
31452
  constructor(hls, fragmentTracker, keyLoader) {
31256
31453
  super(hls, fragmentTracker, keyLoader, 'stream-controller', PlaylistLevelType.MAIN);
31257
31454
  this.audioCodecSwap = false;
31258
- this.gapController = null;
31259
31455
  this.level = -1;
31260
31456
  this._forceStartLoad = false;
31261
31457
  this._hasEnoughToStart = false;
@@ -31267,19 +31463,8 @@ class StreamController extends BaseStreamController {
31267
31463
  this.backtrackFragment = null;
31268
31464
  this.audioCodecSwitch = false;
31269
31465
  this.videoBuffer = null;
31270
- this.onMediaWaiting = () => {
31271
- const gapController = this.gapController;
31272
- if (gapController) {
31273
- gapController.waiting = self.performance.now();
31274
- }
31275
- };
31276
31466
  this.onMediaPlaying = () => {
31277
31467
  // tick to speed up FRAG_CHANGED triggering
31278
- const gapController = this.gapController;
31279
- if (gapController) {
31280
- gapController.ended = 0;
31281
- gapController.waiting = 0;
31282
- }
31283
31468
  this.tick();
31284
31469
  };
31285
31470
  this.onMediaSeeked = () => {
@@ -31334,7 +31519,7 @@ class StreamController extends BaseStreamController {
31334
31519
  }
31335
31520
  onHandlerDestroying() {
31336
31521
  // @ts-ignore
31337
- this.onMediaPlaying = this.onMediaSeeked = this.onMediaWaiting = null;
31522
+ this.onMediaPlaying = this.onMediaSeeked = null;
31338
31523
  this.unregisterListeners();
31339
31524
  super.onHandlerDestroying();
31340
31525
  }
@@ -31429,8 +31614,11 @@ class StreamController extends BaseStreamController {
31429
31614
  this.onTickEnd();
31430
31615
  }
31431
31616
  onTickEnd() {
31617
+ var _this$media2;
31432
31618
  super.onTickEnd();
31433
- this.checkBuffer();
31619
+ if ((_this$media2 = this.media) != null && _this$media2.readyState) {
31620
+ this.lastCurrentTime = this.media.currentTime;
31621
+ }
31434
31622
  this.checkFragmentChanged();
31435
31623
  }
31436
31624
  doTickIdle() {
@@ -31663,29 +31851,19 @@ class StreamController extends BaseStreamController {
31663
31851
  onMediaAttached(event, data) {
31664
31852
  super.onMediaAttached(event, data);
31665
31853
  const media = data.media;
31666
- media.removeEventListener('playing', this.onMediaPlaying);
31667
- media.removeEventListener('seeked', this.onMediaSeeked);
31668
- media.removeEventListener('waiting', this.onMediaWaiting);
31669
- media.addEventListener('playing', this.onMediaPlaying);
31670
- media.addEventListener('seeked', this.onMediaSeeked);
31671
- media.addEventListener('waiting', this.onMediaWaiting);
31672
- this.gapController = new GapController(media, this.fragmentTracker, this.hls);
31854
+ addEventListener(media, 'playing', this.onMediaPlaying);
31855
+ addEventListener(media, 'seeked', this.onMediaSeeked);
31673
31856
  }
31674
31857
  onMediaDetaching(event, data) {
31675
31858
  const {
31676
31859
  media
31677
31860
  } = this;
31678
31861
  if (media) {
31679
- media.removeEventListener('playing', this.onMediaPlaying);
31680
- media.removeEventListener('seeked', this.onMediaSeeked);
31681
- media.removeEventListener('waiting', this.onMediaWaiting);
31862
+ removeEventListener(media, 'playing', this.onMediaPlaying);
31863
+ removeEventListener(media, 'seeked', this.onMediaSeeked);
31682
31864
  }
31683
31865
  this.videoBuffer = null;
31684
31866
  this.fragPlaying = null;
31685
- if (this.gapController) {
31686
- this.gapController.destroy();
31687
- this.gapController = null;
31688
- }
31689
31867
  super.onMediaDetaching(event, data);
31690
31868
  const transferringMedia = !!data.transferMedia;
31691
31869
  if (transferringMedia) {
@@ -31693,19 +31871,6 @@ class StreamController extends BaseStreamController {
31693
31871
  }
31694
31872
  this._hasEnoughToStart = false;
31695
31873
  }
31696
- triggerEnded() {
31697
- const gapController = this.gapController;
31698
- if (gapController) {
31699
- var _this$media2;
31700
- if (gapController.ended) {
31701
- return;
31702
- }
31703
- gapController.ended = ((_this$media2 = this.media) == null ? undefined : _this$media2.currentTime) || 1;
31704
- }
31705
- this.hls.trigger(Events.MEDIA_ENDED, {
31706
- stalled: false
31707
- });
31708
- }
31709
31874
  onManifestLoading() {
31710
31875
  super.onManifestLoading();
31711
31876
  // reset buffer on manifest loading
@@ -32040,26 +32205,6 @@ class StreamController extends BaseStreamController {
32040
32205
  break;
32041
32206
  }
32042
32207
  }
32043
-
32044
- // Checks the health of the buffer and attempts to resolve playback stalls.
32045
- checkBuffer() {
32046
- const {
32047
- media,
32048
- gapController
32049
- } = this;
32050
- if (!media || !gapController || !media.readyState) {
32051
- // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
32052
- return;
32053
- }
32054
- if (this._hasEnoughToStart || !BufferHelper.getBuffered(media).length) {
32055
- // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
32056
- const state = this.state;
32057
- const activeFrag = state !== State.IDLE ? this.fragCurrent : null;
32058
- const levelDetails = this.getLevelDetails();
32059
- gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state);
32060
- }
32061
- this.lastCurrentTime = media.currentTime;
32062
- }
32063
32208
  onFragLoadEmergencyAborted() {
32064
32209
  this.state = State.IDLE;
32065
32210
  // if loadedmetadata is not set, it means that we are emergency switch down on first frag
@@ -32075,8 +32220,10 @@ class StreamController extends BaseStreamController {
32075
32220
  }) {
32076
32221
  if (type !== ElementaryStreamTypes.AUDIO || !this.altAudio) {
32077
32222
  const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media;
32078
- this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
32079
- this.tick();
32223
+ if (mediaBuffer) {
32224
+ this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
32225
+ this.tick();
32226
+ }
32080
32227
  }
32081
32228
  }
32082
32229
  onLevelsUpdated(event, data) {
@@ -33456,9 +33603,12 @@ class Hls {
33456
33603
  this.latencyController = undefined;
33457
33604
  this.levelController = undefined;
33458
33605
  this.streamController = undefined;
33606
+ this.audioStreamController = undefined;
33607
+ this.subtititleStreamController = undefined;
33459
33608
  this.audioTrackController = undefined;
33460
33609
  this.subtitleTrackController = undefined;
33461
33610
  this.interstitialsController = undefined;
33611
+ this.gapController = undefined;
33462
33612
  this.emeController = undefined;
33463
33613
  this.cmcdController = undefined;
33464
33614
  this._media = null;
@@ -33498,6 +33648,7 @@ class Hls {
33498
33648
  const id3TrackController = new ID3TrackController(this);
33499
33649
  const keyLoader = new KeyLoader(this.config);
33500
33650
  const streamController = this.streamController = new StreamController(this, fragmentTracker, keyLoader);
33651
+ const gapController = this.gapController = new GapController(this, fragmentTracker);
33501
33652
 
33502
33653
  // Cap level controller uses streamController to flush the buffer
33503
33654
  capLevelController.setStreamController(streamController);
@@ -33511,17 +33662,17 @@ class Hls {
33511
33662
  networkControllers.splice(1, 0, contentSteering);
33512
33663
  }
33513
33664
  this.networkControllers = networkControllers;
33514
- const coreComponents = [abrController, bufferController, capLevelController, fpsController, id3TrackController, fragmentTracker];
33665
+ const coreComponents = [abrController, bufferController, gapController, capLevelController, fpsController, id3TrackController, fragmentTracker];
33515
33666
  this.audioTrackController = this.createController(config.audioTrackController, networkControllers);
33516
33667
  const AudioStreamControllerClass = config.audioStreamController;
33517
33668
  if (AudioStreamControllerClass) {
33518
- networkControllers.push(new AudioStreamControllerClass(this, fragmentTracker, keyLoader));
33669
+ networkControllers.push(this.audioStreamController = new AudioStreamControllerClass(this, fragmentTracker, keyLoader));
33519
33670
  }
33520
33671
  // Instantiate subtitleTrackController before SubtitleStreamController to receive level events first
33521
33672
  this.subtitleTrackController = this.createController(config.subtitleTrackController, networkControllers);
33522
33673
  const SubtitleStreamControllerClass = config.subtitleStreamController;
33523
33674
  if (SubtitleStreamControllerClass) {
33524
- networkControllers.push(new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader));
33675
+ networkControllers.push(this.subtititleStreamController = new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader));
33525
33676
  }
33526
33677
  this.createController(config.timelineController, coreComponents);
33527
33678
  keyLoader.emeController = this.emeController = this.createController(config.emeController, coreComponents);
@@ -33788,6 +33939,18 @@ class Hls {
33788
33939
  });
33789
33940
  }
33790
33941
  }
33942
+ get inFlightFragments() {
33943
+ const inFlightData = {
33944
+ [PlaylistLevelType.MAIN]: this.streamController.inFlightFrag
33945
+ };
33946
+ if (this.audioStreamController) {
33947
+ inFlightData[PlaylistLevelType.AUDIO] = this.audioStreamController.inFlightFrag;
33948
+ }
33949
+ if (this.subtititleStreamController) {
33950
+ inFlightData[PlaylistLevelType.SUBTITLE] = this.subtititleStreamController.inFlightFrag;
33951
+ }
33952
+ return inFlightData;
33953
+ }
33791
33954
 
33792
33955
  /**
33793
33956
  * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)