hls.js 1.6.0-beta.2.0.canary.10882 → 1.6.0-beta.2.0.canary.10883

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.
@@ -77,6 +77,7 @@ let Events = /*#__PURE__*/function (Events) {
77
77
  Events["MEDIA_DETACHING"] = "hlsMediaDetaching";
78
78
  Events["MEDIA_DETACHED"] = "hlsMediaDetached";
79
79
  Events["MEDIA_ENDED"] = "hlsMediaEnded";
80
+ Events["STALL_RESOLVED"] = "hlsStallResolved";
80
81
  Events["BUFFER_RESET"] = "hlsBufferReset";
81
82
  Events["BUFFER_CODECS"] = "hlsBufferCodecs";
82
83
  Events["BUFFER_CREATED"] = "hlsBufferCreated";
@@ -401,7 +402,7 @@ function enableLogs(debugConfig, context, id) {
401
402
  // Some browsers don't allow to use bind on console object anyway
402
403
  // fallback to default if needed
403
404
  try {
404
- newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.6.0-beta.2.0.canary.10882"}`);
405
+ newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.6.0-beta.2.0.canary.10883"}`);
405
406
  } catch (e) {
406
407
  /* log fn threw an exception. All logger methods are no-ops. */
407
408
  return createLogger();
@@ -7283,8 +7284,7 @@ class BufferHelper {
7283
7284
  return {
7284
7285
  len: 0,
7285
7286
  start: pos,
7286
- end: pos,
7287
- nextStart: undefined
7287
+ end: pos
7288
7288
  };
7289
7289
  }
7290
7290
  static bufferedInfo(buffered, pos, maxHoleDuration) {
@@ -7347,7 +7347,8 @@ class BufferHelper {
7347
7347
  len: bufferLen,
7348
7348
  start: bufferStart || 0,
7349
7349
  end: bufferEnd || 0,
7350
- nextStart: bufferStartNext
7350
+ nextStart: bufferStartNext,
7351
+ buffered
7351
7352
  };
7352
7353
  }
7353
7354
 
@@ -17283,6 +17284,7 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
17283
17284
  progressive: false,
17284
17285
  lowLatencyMode: true,
17285
17286
  cmcd: undefined,
17287
+ detectStallWithCurrentTimeMs: 1250,
17286
17288
  enableDateRangeMetadataCues: true,
17287
17289
  enableEmsgMetadataCues: true,
17288
17290
  enableEmsgKLVMetadata: false,
@@ -18768,32 +18770,28 @@ function assignTrackIdsByGroup(tracks) {
18768
18770
  });
18769
18771
  }
18770
18772
 
18771
- const STALL_MINIMUM_DURATION_MS = 250;
18772
18773
  const MAX_START_GAP_JUMP = 2.0;
18773
18774
  const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
18774
18775
  const SKIP_BUFFER_RANGE_START = 0.05;
18775
18776
  class GapController extends Logger {
18776
- constructor(config, media, fragmentTracker, hls) {
18777
+ constructor(media, fragmentTracker, hls) {
18777
18778
  super('gap-controller', hls.logger);
18778
- this.config = undefined;
18779
18779
  this.media = null;
18780
- this.fragmentTracker = undefined;
18781
- this.hls = undefined;
18780
+ this.fragmentTracker = null;
18781
+ this.hls = null;
18782
18782
  this.nudgeRetry = 0;
18783
18783
  this.stallReported = false;
18784
18784
  this.stalled = null;
18785
18785
  this.moved = false;
18786
18786
  this.seeking = false;
18787
18787
  this.ended = 0;
18788
- this.config = config;
18788
+ this.waiting = 0;
18789
18789
  this.media = media;
18790
18790
  this.fragmentTracker = fragmentTracker;
18791
18791
  this.hls = hls;
18792
18792
  }
18793
18793
  destroy() {
18794
- this.media = null;
18795
- // @ts-ignore
18796
- this.hls = this.fragmentTracker = null;
18794
+ this.media = this.hls = this.fragmentTracker = null;
18797
18795
  }
18798
18796
 
18799
18797
  /**
@@ -18803,12 +18801,12 @@ class GapController extends Logger {
18803
18801
  * @param lastCurrentTime - Previously read playhead position
18804
18802
  */
18805
18803
  poll(lastCurrentTime, activeFrag, levelDetails, state) {
18804
+ var _this$hls;
18806
18805
  const {
18807
- config,
18808
18806
  media,
18809
18807
  stalled
18810
18808
  } = this;
18811
- if (media === null) {
18809
+ if (!media) {
18812
18810
  return;
18813
18811
  }
18814
18812
  const {
@@ -18828,43 +18826,45 @@ class GapController extends Logger {
18828
18826
  if (!seeking) {
18829
18827
  this.nudgeRetry = 0;
18830
18828
  }
18831
- if (stalled !== null) {
18832
- // The playhead is now moving, but was previously stalled
18833
- if (this.stallReported) {
18834
- const _stalledDuration = self.performance.now() - stalled;
18835
- this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(_stalledDuration)}ms`);
18836
- this.stallReported = false;
18837
- }
18838
- this.stalled = null;
18829
+ if (this.waiting === 0) {
18830
+ this.stallResolved(currentTime);
18839
18831
  }
18840
18832
  return;
18841
18833
  }
18842
18834
 
18843
18835
  // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek
18844
18836
  if (beginSeek || seeked) {
18845
- this.stalled = null;
18837
+ if (seeked) {
18838
+ this.stallResolved(currentTime);
18839
+ }
18846
18840
  return;
18847
18841
  }
18848
18842
 
18849
18843
  // The playhead should not be moving
18850
- if (media.paused && !seeking || media.ended || media.playbackRate === 0 || !BufferHelper.getBuffered(media).length) {
18844
+ if (media.paused && !seeking || media.ended || media.playbackRate === 0) {
18845
+ this.nudgeRetry = 0;
18846
+ this.stallResolved(currentTime);
18851
18847
  // Fire MEDIA_ENDED to workaround event not being dispatched by browser
18852
- if (!this.ended && media.ended) {
18848
+ if (!this.ended && media.ended && this.hls) {
18853
18849
  this.ended = currentTime || 1;
18854
18850
  this.hls.trigger(Events.MEDIA_ENDED, {
18855
18851
  stalled: false
18856
18852
  });
18857
18853
  }
18854
+ return;
18855
+ }
18856
+ if (!BufferHelper.getBuffered(media).length) {
18858
18857
  this.nudgeRetry = 0;
18859
18858
  return;
18860
18859
  }
18861
18860
  const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
18862
18861
  const nextStart = bufferInfo.nextStart || 0;
18863
- if (seeking) {
18862
+ const fragmentTracker = this.fragmentTracker;
18863
+ if (seeking && fragmentTracker) {
18864
18864
  // Waiting for seeking in a buffered range to complete
18865
18865
  const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
18866
18866
  // Next buffered range is too far ahead to jump to while still seeking
18867
- const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !this.fragmentTracker.getPartialFragment(currentTime);
18867
+ const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime);
18868
18868
  if (hasEnoughBuffer || noBufferGap) {
18869
18869
  return;
18870
18870
  }
@@ -18874,7 +18874,7 @@ class GapController extends Logger {
18874
18874
 
18875
18875
  // Skip start gaps if we haven't played, but the last poll detected the start of a stall
18876
18876
  // The addition poll gives the browser a chance to jump the gap for us
18877
- if (!this.moved && this.stalled !== null) {
18877
+ if (!this.moved && this.stalled !== null && fragmentTracker) {
18878
18878
  // There is no playable buffer (seeked, waiting for buffer)
18879
18879
  const isBuffered = bufferInfo.len > 0;
18880
18880
  if (!isBuffered && !nextStart) {
@@ -18888,7 +18888,7 @@ class GapController extends Logger {
18888
18888
  // that begins over 1 target duration after the video start position.
18889
18889
  const isLive = !!(levelDetails != null && levelDetails.live);
18890
18890
  const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
18891
- const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
18891
+ const partialOrGap = fragmentTracker.getPartialFragment(currentTime);
18892
18892
  if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
18893
18893
  if (!media.paused) {
18894
18894
  this._trySkipBufferHole(partialOrGap);
@@ -18898,16 +18898,27 @@ class GapController extends Logger {
18898
18898
  }
18899
18899
 
18900
18900
  // Start tracking stall time
18901
+ const config = (_this$hls = this.hls) == null ? undefined : _this$hls.config;
18902
+ if (!config) {
18903
+ return;
18904
+ }
18905
+ const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs;
18901
18906
  const tnow = self.performance.now();
18907
+ const tWaiting = this.waiting;
18902
18908
  if (stalled === null) {
18903
- this.stalled = tnow;
18909
+ // Use time of recent "waiting" event
18910
+ if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) {
18911
+ this.stalled = tWaiting;
18912
+ } else {
18913
+ this.stalled = tnow;
18914
+ }
18904
18915
  return;
18905
18916
  }
18906
18917
  const stalledDuration = tnow - stalled;
18907
- if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
18918
+ if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) {
18908
18919
  // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
18909
18920
  if (state === State.ENDED && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? undefined : levelDetails.edge) || 0)) < 1) {
18910
- if (stalledDuration < 1000 || this.ended) {
18921
+ if (this.ended) {
18911
18922
  return;
18912
18923
  }
18913
18924
  this.ended = currentTime || 1;
@@ -18918,13 +18929,27 @@ class GapController extends Logger {
18918
18929
  }
18919
18930
  // Report stalling after trying to fix
18920
18931
  this._reportStall(bufferInfo);
18921
- if (!this.media) {
18932
+ if (!this.media || !this.hls) {
18922
18933
  return;
18923
18934
  }
18924
18935
  }
18925
18936
  const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
18926
18937
  this._tryFixBufferStall(bufferedWithHoles, stalledDuration);
18927
18938
  }
18939
+ stallResolved(currentTime) {
18940
+ const stalled = this.stalled;
18941
+ if (stalled && this.hls) {
18942
+ this.stalled = null;
18943
+ // The playhead is now moving, but was previously stalled
18944
+ if (this.stallReported) {
18945
+ const stalledDuration = self.performance.now() - stalled;
18946
+ this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`);
18947
+ this.stallReported = false;
18948
+ this.waiting = 0;
18949
+ this.hls.trigger(Events.STALL_RESOLVED, {});
18950
+ }
18951
+ }
18952
+ }
18928
18953
 
18929
18954
  /**
18930
18955
  * Detects and attempts to fix known buffer stalling issues.
@@ -18933,12 +18958,13 @@ class GapController extends Logger {
18933
18958
  * @private
18934
18959
  */
18935
18960
  _tryFixBufferStall(bufferInfo, stalledDurationMs) {
18961
+ var _this$hls2;
18936
18962
  const {
18937
- config,
18938
18963
  fragmentTracker,
18939
18964
  media
18940
18965
  } = this;
18941
- if (media === null) {
18966
+ const config = (_this$hls2 = this.hls) == null ? undefined : _this$hls2.config;
18967
+ if (!media || !fragmentTracker || !config) {
18942
18968
  return;
18943
18969
  }
18944
18970
  const currentTime = media.currentTime;
@@ -18958,13 +18984,12 @@ class GapController extends Logger {
18958
18984
  // we may just have to "nudge" the playlist as the browser decoding/rendering engine
18959
18985
  // needs to cross some sort of threshold covering all source-buffers content
18960
18986
  // to start playing properly.
18961
- if ((bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
18987
+ const bufferedRanges = bufferInfo.buffered;
18988
+ if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
18962
18989
  this.warn('Trying to nudge playhead over buffer-hole');
18963
18990
  // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
18964
18991
  // We only try to jump the hole if it's under the configured size
18965
- // Reset stalled so to rearm watchdog timer
18966
- this.stalled = null;
18967
- this._tryNudgeBuffer();
18992
+ this._tryNudgeBuffer(bufferInfo);
18968
18993
  }
18969
18994
  }
18970
18995
 
@@ -18977,9 +19002,10 @@ class GapController extends Logger {
18977
19002
  const {
18978
19003
  hls,
18979
19004
  media,
18980
- stallReported
19005
+ stallReported,
19006
+ stalled
18981
19007
  } = this;
18982
- if (!stallReported && media) {
19008
+ if (!stallReported && stalled !== null && media && hls) {
18983
19009
  // Report stalled error once
18984
19010
  this.stallReported = true;
18985
19011
  const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
@@ -18989,7 +19015,11 @@ class GapController extends Logger {
18989
19015
  details: ErrorDetails.BUFFER_STALLED_ERROR,
18990
19016
  fatal: false,
18991
19017
  error,
18992
- buffer: bufferInfo.len
19018
+ buffer: bufferInfo.len,
19019
+ bufferInfo,
19020
+ stalled: {
19021
+ start: stalled
19022
+ }
18993
19023
  });
18994
19024
  }
18995
19025
  }
@@ -19000,12 +19030,13 @@ class GapController extends Logger {
19000
19030
  * @private
19001
19031
  */
19002
19032
  _trySkipBufferHole(partial) {
19033
+ var _this$hls3;
19003
19034
  const {
19004
- config,
19005
- hls,
19035
+ fragmentTracker,
19006
19036
  media
19007
19037
  } = this;
19008
- if (media === null) {
19038
+ const config = (_this$hls3 = this.hls) == null ? undefined : _this$hls3.config;
19039
+ if (!media || !fragmentTracker || !config) {
19009
19040
  return 0;
19010
19041
  }
19011
19042
 
@@ -19020,9 +19051,6 @@ class GapController extends Logger {
19020
19051
  if (gapLength > 0 && (bufferStarved || waiting)) {
19021
19052
  // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
19022
19053
  if (gapLength > config.maxBufferHole) {
19023
- const {
19024
- fragmentTracker
19025
- } = this;
19026
19054
  let startGap = false;
19027
19055
  if (currentTime === 0) {
19028
19056
  const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN);
@@ -19053,17 +19081,18 @@ class GapController extends Logger {
19053
19081
  const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
19054
19082
  this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
19055
19083
  this.moved = true;
19056
- this.stalled = null;
19057
19084
  media.currentTime = targetTime;
19058
- if (partial && !partial.gap) {
19085
+ if (partial && !partial.gap && this.hls) {
19059
19086
  const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`);
19060
- hls.trigger(Events.ERROR, {
19087
+ this.hls.trigger(Events.ERROR, {
19061
19088
  type: ErrorTypes.MEDIA_ERROR,
19062
19089
  details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
19063
19090
  fatal: false,
19064
19091
  error,
19065
19092
  reason: error.message,
19066
- frag: partial
19093
+ frag: partial,
19094
+ buffer: bufferInfo.len,
19095
+ bufferInfo
19067
19096
  });
19068
19097
  }
19069
19098
  return targetTime;
@@ -19076,15 +19105,15 @@ class GapController extends Logger {
19076
19105
  * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
19077
19106
  * @private
19078
19107
  */
19079
- _tryNudgeBuffer() {
19108
+ _tryNudgeBuffer(bufferInfo) {
19080
19109
  const {
19081
- config,
19082
19110
  hls,
19083
19111
  media,
19084
19112
  nudgeRetry
19085
19113
  } = this;
19086
- if (media === null) {
19087
- return;
19114
+ const config = hls == null ? undefined : hls.config;
19115
+ if (!media || !config) {
19116
+ return 0;
19088
19117
  }
19089
19118
  const currentTime = media.currentTime;
19090
19119
  this.nudgeRetry++;
@@ -19098,7 +19127,9 @@ class GapController extends Logger {
19098
19127
  type: ErrorTypes.MEDIA_ERROR,
19099
19128
  details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
19100
19129
  error,
19101
- fatal: false
19130
+ fatal: false,
19131
+ buffer: bufferInfo.len,
19132
+ bufferInfo
19102
19133
  });
19103
19134
  } else {
19104
19135
  const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
@@ -19107,13 +19138,15 @@ class GapController extends Logger {
19107
19138
  type: ErrorTypes.MEDIA_ERROR,
19108
19139
  details: ErrorDetails.BUFFER_STALLED_ERROR,
19109
19140
  error,
19110
- fatal: true
19141
+ fatal: true,
19142
+ buffer: bufferInfo.len,
19143
+ bufferInfo
19111
19144
  });
19112
19145
  }
19113
19146
  }
19114
19147
  }
19115
19148
 
19116
- const version = "1.6.0-beta.2.0.canary.10882";
19149
+ const version = "1.6.0-beta.2.0.canary.10883";
19117
19150
 
19118
19151
  // ensure the worker ends up in the bundle
19119
19152
  // If the worker should not be included this gets aliased to empty.js
@@ -19540,11 +19573,18 @@ class StreamController extends BaseStreamController {
19540
19573
  this.backtrackFragment = null;
19541
19574
  this.audioCodecSwitch = false;
19542
19575
  this.videoBuffer = null;
19576
+ this.onMediaWaiting = () => {
19577
+ const gapController = this.gapController;
19578
+ if (gapController) {
19579
+ gapController.waiting = self.performance.now();
19580
+ }
19581
+ };
19543
19582
  this.onMediaPlaying = () => {
19544
19583
  // tick to speed up FRAG_CHANGED triggering
19545
19584
  const gapController = this.gapController;
19546
19585
  if (gapController) {
19547
19586
  gapController.ended = 0;
19587
+ gapController.waiting = 0;
19548
19588
  }
19549
19589
  this.tick();
19550
19590
  };
@@ -19600,7 +19640,7 @@ class StreamController extends BaseStreamController {
19600
19640
  }
19601
19641
  onHandlerDestroying() {
19602
19642
  // @ts-ignore
19603
- this.onMediaPlaying = this.onMediaSeeked = null;
19643
+ this.onMediaPlaying = this.onMediaSeeked = this.onMediaWaiting = null;
19604
19644
  this.unregisterListeners();
19605
19645
  super.onHandlerDestroying();
19606
19646
  }
@@ -19931,9 +19971,11 @@ class StreamController extends BaseStreamController {
19931
19971
  const media = data.media;
19932
19972
  media.removeEventListener('playing', this.onMediaPlaying);
19933
19973
  media.removeEventListener('seeked', this.onMediaSeeked);
19974
+ media.removeEventListener('waiting', this.onMediaWaiting);
19934
19975
  media.addEventListener('playing', this.onMediaPlaying);
19935
19976
  media.addEventListener('seeked', this.onMediaSeeked);
19936
- this.gapController = new GapController(this.config, media, this.fragmentTracker, this.hls);
19977
+ media.addEventListener('waiting', this.onMediaWaiting);
19978
+ this.gapController = new GapController(media, this.fragmentTracker, this.hls);
19937
19979
  }
19938
19980
  onMediaDetaching(event, data) {
19939
19981
  const {
@@ -19942,6 +19984,7 @@ class StreamController extends BaseStreamController {
19942
19984
  if (media) {
19943
19985
  media.removeEventListener('playing', this.onMediaPlaying);
19944
19986
  media.removeEventListener('seeked', this.onMediaSeeked);
19987
+ media.removeEventListener('waiting', this.onMediaWaiting);
19945
19988
  }
19946
19989
  this.videoBuffer = null;
19947
19990
  this.fragPlaying = null;
@@ -20369,7 +20412,7 @@ class StreamController extends BaseStreamController {
20369
20412
  let startPosition = this.startPosition;
20370
20413
  // only adjust currentTime if different from startPosition or if startPosition not buffered
20371
20414
  // at that stage, there should be only one buffered range, as we reach that code after first fragment has been buffered
20372
- if (startPosition >= 0) {
20415
+ if (startPosition >= 0 && currentTime < startPosition) {
20373
20416
  if (media.seeking) {
20374
20417
  this.log(`could not seek to ${startPosition}, already seeking at ${currentTime}`);
20375
20418
  return;