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