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.
package/dist/hls.mjs CHANGED
@@ -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();
@@ -5862,8 +5863,7 @@ class BufferHelper {
5862
5863
  return {
5863
5864
  len: 0,
5864
5865
  start: pos,
5865
- end: pos,
5866
- nextStart: undefined
5866
+ end: pos
5867
5867
  };
5868
5868
  }
5869
5869
  static bufferedInfo(buffered, pos, maxHoleDuration) {
@@ -5926,7 +5926,8 @@ class BufferHelper {
5926
5926
  len: bufferLen,
5927
5927
  start: bufferStart || 0,
5928
5928
  end: bufferEnd || 0,
5929
- nextStart: bufferStartNext
5929
+ nextStart: bufferStartNext,
5930
+ buffered
5930
5931
  };
5931
5932
  }
5932
5933
 
@@ -9847,7 +9848,7 @@ var eventemitter3 = {exports: {}};
9847
9848
  var eventemitter3Exports = eventemitter3.exports;
9848
9849
  var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports);
9849
9850
 
9850
- const version = "1.6.0-beta.2.0.canary.10882";
9851
+ const version = "1.6.0-beta.2.0.canary.10883";
9851
9852
 
9852
9853
  // ensure the worker ends up in the bundle
9853
9854
  // If the worker should not be included this gets aliased to empty.js
@@ -29443,6 +29444,7 @@ const hlsDefaultConfig = _objectSpread2(_objectSpread2({
29443
29444
  progressive: false,
29444
29445
  lowLatencyMode: true,
29445
29446
  cmcd: undefined,
29447
+ detectStallWithCurrentTimeMs: 1250,
29446
29448
  enableDateRangeMetadataCues: true,
29447
29449
  enableEmsgMetadataCues: true,
29448
29450
  enableEmsgKLVMetadata: false,
@@ -30851,32 +30853,28 @@ function assignTrackIdsByGroup(tracks) {
30851
30853
  });
30852
30854
  }
30853
30855
 
30854
- const STALL_MINIMUM_DURATION_MS = 250;
30855
30856
  const MAX_START_GAP_JUMP = 2.0;
30856
30857
  const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
30857
30858
  const SKIP_BUFFER_RANGE_START = 0.05;
30858
30859
  class GapController extends Logger {
30859
- constructor(config, media, fragmentTracker, hls) {
30860
+ constructor(media, fragmentTracker, hls) {
30860
30861
  super('gap-controller', hls.logger);
30861
- this.config = undefined;
30862
30862
  this.media = null;
30863
- this.fragmentTracker = undefined;
30864
- this.hls = undefined;
30863
+ this.fragmentTracker = null;
30864
+ this.hls = null;
30865
30865
  this.nudgeRetry = 0;
30866
30866
  this.stallReported = false;
30867
30867
  this.stalled = null;
30868
30868
  this.moved = false;
30869
30869
  this.seeking = false;
30870
30870
  this.ended = 0;
30871
- this.config = config;
30871
+ this.waiting = 0;
30872
30872
  this.media = media;
30873
30873
  this.fragmentTracker = fragmentTracker;
30874
30874
  this.hls = hls;
30875
30875
  }
30876
30876
  destroy() {
30877
- this.media = null;
30878
- // @ts-ignore
30879
- this.hls = this.fragmentTracker = null;
30877
+ this.media = this.hls = this.fragmentTracker = null;
30880
30878
  }
30881
30879
 
30882
30880
  /**
@@ -30886,12 +30884,12 @@ class GapController extends Logger {
30886
30884
  * @param lastCurrentTime - Previously read playhead position
30887
30885
  */
30888
30886
  poll(lastCurrentTime, activeFrag, levelDetails, state) {
30887
+ var _this$hls;
30889
30888
  const {
30890
- config,
30891
30889
  media,
30892
30890
  stalled
30893
30891
  } = this;
30894
- if (media === null) {
30892
+ if (!media) {
30895
30893
  return;
30896
30894
  }
30897
30895
  const {
@@ -30911,43 +30909,45 @@ class GapController extends Logger {
30911
30909
  if (!seeking) {
30912
30910
  this.nudgeRetry = 0;
30913
30911
  }
30914
- if (stalled !== null) {
30915
- // The playhead is now moving, but was previously stalled
30916
- if (this.stallReported) {
30917
- const _stalledDuration = self.performance.now() - stalled;
30918
- this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(_stalledDuration)}ms`);
30919
- this.stallReported = false;
30920
- }
30921
- this.stalled = null;
30912
+ if (this.waiting === 0) {
30913
+ this.stallResolved(currentTime);
30922
30914
  }
30923
30915
  return;
30924
30916
  }
30925
30917
 
30926
30918
  // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek
30927
30919
  if (beginSeek || seeked) {
30928
- this.stalled = null;
30920
+ if (seeked) {
30921
+ this.stallResolved(currentTime);
30922
+ }
30929
30923
  return;
30930
30924
  }
30931
30925
 
30932
30926
  // The playhead should not be moving
30933
- if (media.paused && !seeking || media.ended || media.playbackRate === 0 || !BufferHelper.getBuffered(media).length) {
30927
+ if (media.paused && !seeking || media.ended || media.playbackRate === 0) {
30928
+ this.nudgeRetry = 0;
30929
+ this.stallResolved(currentTime);
30934
30930
  // Fire MEDIA_ENDED to workaround event not being dispatched by browser
30935
- if (!this.ended && media.ended) {
30931
+ if (!this.ended && media.ended && this.hls) {
30936
30932
  this.ended = currentTime || 1;
30937
30933
  this.hls.trigger(Events.MEDIA_ENDED, {
30938
30934
  stalled: false
30939
30935
  });
30940
30936
  }
30937
+ return;
30938
+ }
30939
+ if (!BufferHelper.getBuffered(media).length) {
30941
30940
  this.nudgeRetry = 0;
30942
30941
  return;
30943
30942
  }
30944
30943
  const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
30945
30944
  const nextStart = bufferInfo.nextStart || 0;
30946
- if (seeking) {
30945
+ const fragmentTracker = this.fragmentTracker;
30946
+ if (seeking && fragmentTracker) {
30947
30947
  // Waiting for seeking in a buffered range to complete
30948
30948
  const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
30949
30949
  // Next buffered range is too far ahead to jump to while still seeking
30950
- const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !this.fragmentTracker.getPartialFragment(currentTime);
30950
+ const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime);
30951
30951
  if (hasEnoughBuffer || noBufferGap) {
30952
30952
  return;
30953
30953
  }
@@ -30957,7 +30957,7 @@ class GapController extends Logger {
30957
30957
 
30958
30958
  // Skip start gaps if we haven't played, but the last poll detected the start of a stall
30959
30959
  // The addition poll gives the browser a chance to jump the gap for us
30960
- if (!this.moved && this.stalled !== null) {
30960
+ if (!this.moved && this.stalled !== null && fragmentTracker) {
30961
30961
  // There is no playable buffer (seeked, waiting for buffer)
30962
30962
  const isBuffered = bufferInfo.len > 0;
30963
30963
  if (!isBuffered && !nextStart) {
@@ -30971,7 +30971,7 @@ class GapController extends Logger {
30971
30971
  // that begins over 1 target duration after the video start position.
30972
30972
  const isLive = !!(levelDetails != null && levelDetails.live);
30973
30973
  const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP;
30974
- const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
30974
+ const partialOrGap = fragmentTracker.getPartialFragment(currentTime);
30975
30975
  if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
30976
30976
  if (!media.paused) {
30977
30977
  this._trySkipBufferHole(partialOrGap);
@@ -30981,16 +30981,27 @@ class GapController extends Logger {
30981
30981
  }
30982
30982
 
30983
30983
  // Start tracking stall time
30984
+ const config = (_this$hls = this.hls) == null ? undefined : _this$hls.config;
30985
+ if (!config) {
30986
+ return;
30987
+ }
30988
+ const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs;
30984
30989
  const tnow = self.performance.now();
30990
+ const tWaiting = this.waiting;
30985
30991
  if (stalled === null) {
30986
- this.stalled = tnow;
30992
+ // Use time of recent "waiting" event
30993
+ if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) {
30994
+ this.stalled = tWaiting;
30995
+ } else {
30996
+ this.stalled = tnow;
30997
+ }
30987
30998
  return;
30988
30999
  }
30989
31000
  const stalledDuration = tnow - stalled;
30990
- if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
31001
+ if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) {
30991
31002
  // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
30992
31003
  if (state === State.ENDED && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? undefined : levelDetails.edge) || 0)) < 1) {
30993
- if (stalledDuration < 1000 || this.ended) {
31004
+ if (this.ended) {
30994
31005
  return;
30995
31006
  }
30996
31007
  this.ended = currentTime || 1;
@@ -31001,13 +31012,27 @@ class GapController extends Logger {
31001
31012
  }
31002
31013
  // Report stalling after trying to fix
31003
31014
  this._reportStall(bufferInfo);
31004
- if (!this.media) {
31015
+ if (!this.media || !this.hls) {
31005
31016
  return;
31006
31017
  }
31007
31018
  }
31008
31019
  const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
31009
31020
  this._tryFixBufferStall(bufferedWithHoles, stalledDuration);
31010
31021
  }
31022
+ stallResolved(currentTime) {
31023
+ const stalled = this.stalled;
31024
+ if (stalled && this.hls) {
31025
+ this.stalled = null;
31026
+ // The playhead is now moving, but was previously stalled
31027
+ if (this.stallReported) {
31028
+ const stalledDuration = self.performance.now() - stalled;
31029
+ this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`);
31030
+ this.stallReported = false;
31031
+ this.waiting = 0;
31032
+ this.hls.trigger(Events.STALL_RESOLVED, {});
31033
+ }
31034
+ }
31035
+ }
31011
31036
 
31012
31037
  /**
31013
31038
  * Detects and attempts to fix known buffer stalling issues.
@@ -31016,12 +31041,13 @@ class GapController extends Logger {
31016
31041
  * @private
31017
31042
  */
31018
31043
  _tryFixBufferStall(bufferInfo, stalledDurationMs) {
31044
+ var _this$hls2;
31019
31045
  const {
31020
- config,
31021
31046
  fragmentTracker,
31022
31047
  media
31023
31048
  } = this;
31024
- if (media === null) {
31049
+ const config = (_this$hls2 = this.hls) == null ? undefined : _this$hls2.config;
31050
+ if (!media || !fragmentTracker || !config) {
31025
31051
  return;
31026
31052
  }
31027
31053
  const currentTime = media.currentTime;
@@ -31041,13 +31067,12 @@ class GapController extends Logger {
31041
31067
  // we may just have to "nudge" the playlist as the browser decoding/rendering engine
31042
31068
  // needs to cross some sort of threshold covering all source-buffers content
31043
31069
  // to start playing properly.
31044
- if ((bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
31070
+ const bufferedRanges = bufferInfo.buffered;
31071
+ if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
31045
31072
  this.warn('Trying to nudge playhead over buffer-hole');
31046
31073
  // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
31047
31074
  // We only try to jump the hole if it's under the configured size
31048
- // Reset stalled so to rearm watchdog timer
31049
- this.stalled = null;
31050
- this._tryNudgeBuffer();
31075
+ this._tryNudgeBuffer(bufferInfo);
31051
31076
  }
31052
31077
  }
31053
31078
 
@@ -31060,9 +31085,10 @@ class GapController extends Logger {
31060
31085
  const {
31061
31086
  hls,
31062
31087
  media,
31063
- stallReported
31088
+ stallReported,
31089
+ stalled
31064
31090
  } = this;
31065
- if (!stallReported && media) {
31091
+ if (!stallReported && stalled !== null && media && hls) {
31066
31092
  // Report stalled error once
31067
31093
  this.stallReported = true;
31068
31094
  const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`);
@@ -31072,7 +31098,11 @@ class GapController extends Logger {
31072
31098
  details: ErrorDetails.BUFFER_STALLED_ERROR,
31073
31099
  fatal: false,
31074
31100
  error,
31075
- buffer: bufferInfo.len
31101
+ buffer: bufferInfo.len,
31102
+ bufferInfo,
31103
+ stalled: {
31104
+ start: stalled
31105
+ }
31076
31106
  });
31077
31107
  }
31078
31108
  }
@@ -31083,12 +31113,13 @@ class GapController extends Logger {
31083
31113
  * @private
31084
31114
  */
31085
31115
  _trySkipBufferHole(partial) {
31116
+ var _this$hls3;
31086
31117
  const {
31087
- config,
31088
- hls,
31118
+ fragmentTracker,
31089
31119
  media
31090
31120
  } = this;
31091
- if (media === null) {
31121
+ const config = (_this$hls3 = this.hls) == null ? undefined : _this$hls3.config;
31122
+ if (!media || !fragmentTracker || !config) {
31092
31123
  return 0;
31093
31124
  }
31094
31125
 
@@ -31103,9 +31134,6 @@ class GapController extends Logger {
31103
31134
  if (gapLength > 0 && (bufferStarved || waiting)) {
31104
31135
  // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
31105
31136
  if (gapLength > config.maxBufferHole) {
31106
- const {
31107
- fragmentTracker
31108
- } = this;
31109
31137
  let startGap = false;
31110
31138
  if (currentTime === 0) {
31111
31139
  const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN);
@@ -31136,17 +31164,18 @@ class GapController extends Logger {
31136
31164
  const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS);
31137
31165
  this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`);
31138
31166
  this.moved = true;
31139
- this.stalled = null;
31140
31167
  media.currentTime = targetTime;
31141
- if (partial && !partial.gap) {
31168
+ if (partial && !partial.gap && this.hls) {
31142
31169
  const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`);
31143
- hls.trigger(Events.ERROR, {
31170
+ this.hls.trigger(Events.ERROR, {
31144
31171
  type: ErrorTypes.MEDIA_ERROR,
31145
31172
  details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
31146
31173
  fatal: false,
31147
31174
  error,
31148
31175
  reason: error.message,
31149
- frag: partial
31176
+ frag: partial,
31177
+ buffer: bufferInfo.len,
31178
+ bufferInfo
31150
31179
  });
31151
31180
  }
31152
31181
  return targetTime;
@@ -31159,15 +31188,15 @@ class GapController extends Logger {
31159
31188
  * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
31160
31189
  * @private
31161
31190
  */
31162
- _tryNudgeBuffer() {
31191
+ _tryNudgeBuffer(bufferInfo) {
31163
31192
  const {
31164
- config,
31165
31193
  hls,
31166
31194
  media,
31167
31195
  nudgeRetry
31168
31196
  } = this;
31169
- if (media === null) {
31170
- return;
31197
+ const config = hls == null ? undefined : hls.config;
31198
+ if (!media || !config) {
31199
+ return 0;
31171
31200
  }
31172
31201
  const currentTime = media.currentTime;
31173
31202
  this.nudgeRetry++;
@@ -31181,7 +31210,9 @@ class GapController extends Logger {
31181
31210
  type: ErrorTypes.MEDIA_ERROR,
31182
31211
  details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
31183
31212
  error,
31184
- fatal: false
31213
+ fatal: false,
31214
+ buffer: bufferInfo.len,
31215
+ bufferInfo
31185
31216
  });
31186
31217
  } else {
31187
31218
  const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
@@ -31190,7 +31221,9 @@ class GapController extends Logger {
31190
31221
  type: ErrorTypes.MEDIA_ERROR,
31191
31222
  details: ErrorDetails.BUFFER_STALLED_ERROR,
31192
31223
  error,
31193
- fatal: true
31224
+ fatal: true,
31225
+ buffer: bufferInfo.len,
31226
+ bufferInfo
31194
31227
  });
31195
31228
  }
31196
31229
  }
@@ -31241,11 +31274,18 @@ class StreamController extends BaseStreamController {
31241
31274
  this.backtrackFragment = null;
31242
31275
  this.audioCodecSwitch = false;
31243
31276
  this.videoBuffer = null;
31277
+ this.onMediaWaiting = () => {
31278
+ const gapController = this.gapController;
31279
+ if (gapController) {
31280
+ gapController.waiting = self.performance.now();
31281
+ }
31282
+ };
31244
31283
  this.onMediaPlaying = () => {
31245
31284
  // tick to speed up FRAG_CHANGED triggering
31246
31285
  const gapController = this.gapController;
31247
31286
  if (gapController) {
31248
31287
  gapController.ended = 0;
31288
+ gapController.waiting = 0;
31249
31289
  }
31250
31290
  this.tick();
31251
31291
  };
@@ -31301,7 +31341,7 @@ class StreamController extends BaseStreamController {
31301
31341
  }
31302
31342
  onHandlerDestroying() {
31303
31343
  // @ts-ignore
31304
- this.onMediaPlaying = this.onMediaSeeked = null;
31344
+ this.onMediaPlaying = this.onMediaSeeked = this.onMediaWaiting = null;
31305
31345
  this.unregisterListeners();
31306
31346
  super.onHandlerDestroying();
31307
31347
  }
@@ -31632,9 +31672,11 @@ class StreamController extends BaseStreamController {
31632
31672
  const media = data.media;
31633
31673
  media.removeEventListener('playing', this.onMediaPlaying);
31634
31674
  media.removeEventListener('seeked', this.onMediaSeeked);
31675
+ media.removeEventListener('waiting', this.onMediaWaiting);
31635
31676
  media.addEventListener('playing', this.onMediaPlaying);
31636
31677
  media.addEventListener('seeked', this.onMediaSeeked);
31637
- this.gapController = new GapController(this.config, media, this.fragmentTracker, this.hls);
31678
+ media.addEventListener('waiting', this.onMediaWaiting);
31679
+ this.gapController = new GapController(media, this.fragmentTracker, this.hls);
31638
31680
  }
31639
31681
  onMediaDetaching(event, data) {
31640
31682
  const {
@@ -31643,6 +31685,7 @@ class StreamController extends BaseStreamController {
31643
31685
  if (media) {
31644
31686
  media.removeEventListener('playing', this.onMediaPlaying);
31645
31687
  media.removeEventListener('seeked', this.onMediaSeeked);
31688
+ media.removeEventListener('waiting', this.onMediaWaiting);
31646
31689
  }
31647
31690
  this.videoBuffer = null;
31648
31691
  this.fragPlaying = null;
@@ -32070,7 +32113,7 @@ class StreamController extends BaseStreamController {
32070
32113
  let startPosition = this.startPosition;
32071
32114
  // only adjust currentTime if different from startPosition or if startPosition not buffered
32072
32115
  // at that stage, there should be only one buffered range, as we reach that code after first fragment has been buffered
32073
- if (startPosition >= 0) {
32116
+ if (startPosition >= 0 && currentTime < startPosition) {
32074
32117
  if (media.seeking) {
32075
32118
  this.log(`could not seek to ${startPosition}, already seeking at ${currentTime}`);
32076
32119
  return;