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