hls.js 1.5.13 → 1.5.14-0.canary.10417
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/README.md +4 -3
- package/dist/hls-demo.js +41 -38
- package/dist/hls-demo.js.map +1 -1
- package/dist/hls.js +4211 -2666
- package/dist/hls.js.d.ts +179 -110
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +2841 -1921
- 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 +2569 -1639
- 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 +3572 -2017
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/dist/hls.worker.js.map +1 -1
- package/package.json +38 -38
- package/src/config.ts +5 -2
- package/src/controller/abr-controller.ts +39 -25
- package/src/controller/audio-stream-controller.ts +156 -136
- package/src/controller/audio-track-controller.ts +1 -1
- package/src/controller/base-playlist-controller.ts +27 -10
- package/src/controller/base-stream-controller.ts +234 -89
- package/src/controller/buffer-controller.ts +250 -97
- package/src/controller/buffer-operation-queue.ts +16 -19
- package/src/controller/cap-level-controller.ts +3 -2
- package/src/controller/cmcd-controller.ts +51 -14
- package/src/controller/content-steering-controller.ts +29 -15
- package/src/controller/eme-controller.ts +10 -23
- package/src/controller/error-controller.ts +28 -22
- package/src/controller/fps-controller.ts +8 -3
- package/src/controller/fragment-finders.ts +44 -16
- package/src/controller/fragment-tracker.ts +58 -25
- package/src/controller/gap-controller.ts +43 -16
- package/src/controller/id3-track-controller.ts +45 -35
- package/src/controller/latency-controller.ts +18 -13
- package/src/controller/level-controller.ts +37 -19
- package/src/controller/stream-controller.ts +100 -83
- package/src/controller/subtitle-stream-controller.ts +35 -47
- package/src/controller/subtitle-track-controller.ts +5 -3
- package/src/controller/timeline-controller.ts +20 -22
- package/src/crypt/aes-crypto.ts +21 -2
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +32 -16
- package/src/crypt/fast-aes-key.ts +28 -5
- package/src/demux/audio/aacdemuxer.ts +2 -2
- package/src/demux/audio/ac3-demuxer.ts +4 -3
- package/src/demux/audio/adts.ts +9 -4
- package/src/demux/audio/base-audio-demuxer.ts +16 -14
- package/src/demux/audio/mp3demuxer.ts +4 -3
- package/src/demux/audio/mpegaudio.ts +1 -1
- package/src/demux/mp4demuxer.ts +7 -7
- package/src/demux/sample-aes.ts +2 -0
- package/src/demux/transmuxer-interface.ts +8 -16
- package/src/demux/transmuxer-worker.ts +4 -4
- package/src/demux/transmuxer.ts +16 -3
- package/src/demux/tsdemuxer.ts +75 -38
- package/src/demux/video/avc-video-parser.ts +210 -121
- package/src/demux/video/base-video-parser.ts +135 -2
- package/src/demux/video/exp-golomb.ts +0 -208
- package/src/demux/video/hevc-video-parser.ts +749 -0
- package/src/events.ts +8 -1
- package/src/exports-named.ts +1 -1
- package/src/hls.ts +84 -47
- package/src/loader/date-range.ts +71 -5
- package/src/loader/fragment-loader.ts +23 -21
- package/src/loader/fragment.ts +8 -4
- package/src/loader/key-loader.ts +3 -1
- package/src/loader/level-details.ts +6 -6
- package/src/loader/level-key.ts +10 -9
- package/src/loader/m3u8-parser.ts +138 -144
- package/src/loader/playlist-loader.ts +5 -7
- package/src/remux/mp4-generator.ts +196 -1
- package/src/remux/mp4-remuxer.ts +32 -62
- package/src/remux/passthrough-remuxer.ts +1 -1
- package/src/task-loop.ts +5 -2
- package/src/types/component-api.ts +3 -1
- package/src/types/demuxer.ts +3 -0
- package/src/types/events.ts +19 -6
- package/src/types/fragment-tracker.ts +2 -2
- package/src/types/media-playlist.ts +9 -1
- package/src/types/remuxer.ts +1 -1
- package/src/utils/attr-list.ts +96 -9
- package/src/utils/buffer-helper.ts +12 -31
- package/src/utils/cea-608-parser.ts +1 -3
- package/src/utils/codecs.ts +34 -5
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/fetch-loader.ts +1 -1
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +4 -7
- package/src/utils/imsc1-ttml-parser.ts +1 -1
- package/src/utils/keysystem-util.ts +1 -6
- package/src/utils/level-helper.ts +71 -44
- package/src/utils/logger.ts +58 -23
- package/src/utils/mp4-tools.ts +5 -3
- package/src/utils/rendition-helper.ts +100 -74
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +0 -19
- package/src/utils/webvtt-parser.ts +2 -12
- package/src/demux/id3.ts +0 -411
- package/src/types/general.ts +0 -6
@@ -1,12 +1,15 @@
|
|
1
1
|
import TaskLoop from '../task-loop';
|
2
2
|
import { FragmentState } from './fragment-tracker';
|
3
3
|
import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
|
4
|
-
import { logger } from '../utils/logger';
|
5
4
|
import { Events } from '../events';
|
6
5
|
import { ErrorDetails, ErrorTypes } from '../errors';
|
7
6
|
import { ChunkMetadata } from '../types/transmuxer';
|
8
7
|
import { appendUint8Array } from '../utils/mp4-tools';
|
9
8
|
import { alignStream } from '../utils/discontinuities';
|
9
|
+
import {
|
10
|
+
isFullSegmentEncryption,
|
11
|
+
getAesModeFromFullSegmentMethod,
|
12
|
+
} from '../utils/encryption-methods-util';
|
10
13
|
import {
|
11
14
|
findFragmentByPDT,
|
12
15
|
findFragmentByPTS,
|
@@ -19,7 +22,7 @@ import {
|
|
19
22
|
updateFragPTSDTS,
|
20
23
|
} from '../utils/level-helper';
|
21
24
|
import TransmuxerInterface from '../demux/transmuxer-interface';
|
22
|
-
import { Fragment, Part } from '../loader/fragment';
|
25
|
+
import { Fragment, MediaFragment, Part } from '../loader/fragment';
|
23
26
|
import FragmentLoader, {
|
24
27
|
FragmentLoadProgressCallback,
|
25
28
|
LoadError,
|
@@ -74,7 +77,7 @@ export default class BaseStreamController
|
|
74
77
|
{
|
75
78
|
protected hls: Hls;
|
76
79
|
|
77
|
-
protected fragPrevious:
|
80
|
+
protected fragPrevious: MediaFragment | null = null;
|
78
81
|
protected fragCurrent: Fragment | null = null;
|
79
82
|
protected fragmentTracker: FragmentTracker;
|
80
83
|
protected transmuxer: TransmuxerInterface | null = null;
|
@@ -97,12 +100,9 @@ export default class BaseStreamController
|
|
97
100
|
protected startFragRequested: boolean = false;
|
98
101
|
protected decrypter: Decrypter;
|
99
102
|
protected initPTS: RationalTimestamp[] = [];
|
100
|
-
protected
|
101
|
-
protected
|
102
|
-
|
103
|
-
private readonly logPrefix: string = '';
|
104
|
-
protected log: (msg: any) => void;
|
105
|
-
protected warn: (msg: any) => void;
|
103
|
+
protected buffering: boolean = true;
|
104
|
+
protected loadingParts: boolean = false;
|
105
|
+
private loopSn?: string | number;
|
106
106
|
|
107
107
|
constructor(
|
108
108
|
hls: Hls,
|
@@ -111,18 +111,32 @@ export default class BaseStreamController
|
|
111
111
|
logPrefix: string,
|
112
112
|
playlistType: PlaylistLevelType,
|
113
113
|
) {
|
114
|
-
super();
|
114
|
+
super(logPrefix, hls.logger);
|
115
115
|
this.playlistType = playlistType;
|
116
|
-
this.logPrefix = logPrefix;
|
117
|
-
this.log = logger.log.bind(logger, `${logPrefix}:`);
|
118
|
-
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
|
119
116
|
this.hls = hls;
|
120
117
|
this.fragmentLoader = new FragmentLoader(hls.config);
|
121
118
|
this.keyLoader = keyLoader;
|
122
119
|
this.fragmentTracker = fragmentTracker;
|
123
120
|
this.config = hls.config;
|
124
121
|
this.decrypter = new Decrypter(hls.config);
|
122
|
+
}
|
123
|
+
|
124
|
+
protected registerListeners() {
|
125
|
+
const { hls } = this;
|
126
|
+
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
|
127
|
+
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
128
|
+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
125
129
|
hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
130
|
+
hls.on(Events.ERROR, this.onError, this);
|
131
|
+
}
|
132
|
+
|
133
|
+
protected unregisterListeners() {
|
134
|
+
const { hls } = this;
|
135
|
+
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
|
136
|
+
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
137
|
+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
138
|
+
hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
139
|
+
hls.off(Events.ERROR, this.onError, this);
|
126
140
|
}
|
127
141
|
|
128
142
|
protected doTick() {
|
@@ -135,6 +149,9 @@ export default class BaseStreamController
|
|
135
149
|
public startLoad(startPosition: number): void {}
|
136
150
|
|
137
151
|
public stopLoad() {
|
152
|
+
if (this.state === State.STOPPED) {
|
153
|
+
return;
|
154
|
+
}
|
138
155
|
this.fragmentLoader.abort();
|
139
156
|
this.keyLoader.abort(this.playlistType);
|
140
157
|
const frag = this.fragCurrent;
|
@@ -150,6 +167,14 @@ export default class BaseStreamController
|
|
150
167
|
this.state = State.STOPPED;
|
151
168
|
}
|
152
169
|
|
170
|
+
public pauseBuffering() {
|
171
|
+
this.buffering = false;
|
172
|
+
}
|
173
|
+
|
174
|
+
public resumeBuffering() {
|
175
|
+
this.buffering = true;
|
176
|
+
}
|
177
|
+
|
153
178
|
protected _streamEnded(
|
154
179
|
bufferInfo: BufferInfo,
|
155
180
|
levelDetails: LevelDetails,
|
@@ -197,10 +222,8 @@ export default class BaseStreamController
|
|
197
222
|
data: MediaAttachedData,
|
198
223
|
) {
|
199
224
|
const media = (this.media = this.mediaBuffer = data.media);
|
200
|
-
|
201
|
-
|
202
|
-
media.addEventListener('seeking', this.onvseeking);
|
203
|
-
media.addEventListener('ended', this.onvended);
|
225
|
+
media.addEventListener('seeking', this.onMediaSeeking);
|
226
|
+
media.addEventListener('ended', this.onMediaEnded);
|
204
227
|
const config = this.config;
|
205
228
|
if (this.levels && config.autoStartLoad && this.state === State.STOPPED) {
|
206
229
|
this.startLoad(config.startPosition);
|
@@ -215,21 +238,30 @@ export default class BaseStreamController
|
|
215
238
|
}
|
216
239
|
|
217
240
|
// remove video listeners
|
218
|
-
if (media
|
219
|
-
media.removeEventListener('seeking', this.
|
220
|
-
media.removeEventListener('ended', this.
|
221
|
-
this.onvseeking = this.onvended = null;
|
241
|
+
if (media) {
|
242
|
+
media.removeEventListener('seeking', this.onMediaSeeking);
|
243
|
+
media.removeEventListener('ended', this.onMediaEnded);
|
222
244
|
}
|
223
245
|
if (this.keyLoader) {
|
224
246
|
this.keyLoader.detach();
|
225
247
|
}
|
226
248
|
this.media = this.mediaBuffer = null;
|
227
|
-
this.
|
249
|
+
this.loopSn = undefined;
|
250
|
+
this.startFragRequested = this.loadedmetadata = this.loadingParts = false;
|
228
251
|
this.fragmentTracker.removeAllFragments();
|
229
252
|
this.stopLoad();
|
230
253
|
}
|
231
254
|
|
232
|
-
protected
|
255
|
+
protected onManifestLoading() {
|
256
|
+
this.initPTS = [];
|
257
|
+
this.levels = this.levelLastLoaded = this.fragCurrent = null;
|
258
|
+
this.lastCurrentTime = this.startPosition = 0;
|
259
|
+
this.startFragRequested = false;
|
260
|
+
}
|
261
|
+
|
262
|
+
protected onError(event: Events.ERROR, data: ErrorData) {}
|
263
|
+
|
264
|
+
protected onMediaSeeking = () => {
|
233
265
|
const { config, fragCurrent, media, mediaBuffer, state } = this;
|
234
266
|
const currentTime: number = media ? media.currentTime : 0;
|
235
267
|
const bufferInfo = BufferHelper.bufferInfo(
|
@@ -282,7 +314,27 @@ export default class BaseStreamController
|
|
282
314
|
true,
|
283
315
|
);
|
284
316
|
|
285
|
-
|
317
|
+
// Don't set lastCurrentTime with backward seeks (allows for frag selection with strict tolerances)
|
318
|
+
const lastCurrentTime = this.lastCurrentTime;
|
319
|
+
if (currentTime > lastCurrentTime) {
|
320
|
+
this.lastCurrentTime = currentTime;
|
321
|
+
}
|
322
|
+
|
323
|
+
if (!this.loadingParts) {
|
324
|
+
const bufferEnd = Math.max(bufferInfo.end, currentTime);
|
325
|
+
const shouldLoadParts = this.shouldLoadParts(
|
326
|
+
this.getLevelDetails(),
|
327
|
+
bufferEnd,
|
328
|
+
);
|
329
|
+
if (shouldLoadParts) {
|
330
|
+
this.log(
|
331
|
+
`LL-Part loading ON after seeking to ${currentTime.toFixed(
|
332
|
+
2,
|
333
|
+
)} with buffer @${bufferEnd.toFixed(2)}`,
|
334
|
+
);
|
335
|
+
this.loadingParts = shouldLoadParts;
|
336
|
+
}
|
337
|
+
}
|
286
338
|
}
|
287
339
|
|
288
340
|
// in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
|
@@ -292,27 +344,30 @@ export default class BaseStreamController
|
|
292
344
|
|
293
345
|
// Async tick to speed up processing
|
294
346
|
this.tickImmediate();
|
295
|
-
}
|
347
|
+
};
|
296
348
|
|
297
|
-
protected onMediaEnded() {
|
349
|
+
protected onMediaEnded = () => {
|
298
350
|
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
|
299
351
|
this.startPosition = this.lastCurrentTime = 0;
|
300
|
-
|
352
|
+
if (this.playlistType === PlaylistLevelType.MAIN) {
|
353
|
+
this.hls.trigger(Events.MEDIA_ENDED, {
|
354
|
+
stalled: false,
|
355
|
+
});
|
356
|
+
}
|
357
|
+
};
|
301
358
|
|
302
359
|
protected onManifestLoaded(
|
303
360
|
event: Events.MANIFEST_LOADED,
|
304
361
|
data: ManifestLoadedData,
|
305
362
|
): void {
|
306
363
|
this.startTimeOffset = data.startTimeOffset;
|
307
|
-
this.initPTS = [];
|
308
364
|
}
|
309
365
|
|
310
366
|
protected onHandlerDestroying() {
|
311
|
-
this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
312
367
|
this.stopLoad();
|
313
368
|
super.onHandlerDestroying();
|
314
369
|
// @ts-ignore
|
315
|
-
this.hls = null;
|
370
|
+
this.hls = this.onMediaSeeking = this.onMediaEnded = null;
|
316
371
|
}
|
317
372
|
|
318
373
|
protected onHandlerDestroyed() {
|
@@ -343,6 +398,7 @@ export default class BaseStreamController
|
|
343
398
|
level: Level,
|
344
399
|
targetBufferTime: number,
|
345
400
|
) {
|
401
|
+
this.startFragRequested = true;
|
346
402
|
this._loadFragForPlayback(frag, level, targetBufferTime);
|
347
403
|
}
|
348
404
|
|
@@ -496,7 +552,7 @@ export default class BaseStreamController
|
|
496
552
|
payload.byteLength > 0 &&
|
497
553
|
decryptData?.key &&
|
498
554
|
decryptData.iv &&
|
499
|
-
decryptData.method
|
555
|
+
isFullSegmentEncryption(decryptData.method)
|
500
556
|
) {
|
501
557
|
const startTime = self.performance.now();
|
502
558
|
// decrypt init segment data
|
@@ -505,6 +561,7 @@ export default class BaseStreamController
|
|
505
561
|
new Uint8Array(payload),
|
506
562
|
decryptData.key.buffer,
|
507
563
|
decryptData.iv.buffer,
|
564
|
+
getAesModeFromFullSegmentMethod(decryptData.method),
|
508
565
|
)
|
509
566
|
.catch((err) => {
|
510
567
|
hls.trigger(Events.ERROR, {
|
@@ -548,7 +605,9 @@ export default class BaseStreamController
|
|
548
605
|
throw new Error('init load aborted, missing levels');
|
549
606
|
}
|
550
607
|
const stats = data.frag.stats;
|
551
|
-
this.state
|
608
|
+
if (this.state !== State.STOPPED) {
|
609
|
+
this.state = State.IDLE;
|
610
|
+
}
|
552
611
|
data.frag.data = new Uint8Array(data.payload);
|
553
612
|
stats.parsing.start = stats.buffering.start = self.performance.now();
|
554
613
|
stats.parsing.end = stats.buffering.end = self.performance.now();
|
@@ -648,6 +707,7 @@ export default class BaseStreamController
|
|
648
707
|
targetBufferTime: number | null = null,
|
649
708
|
progressCallback?: FragmentLoadProgressCallback,
|
650
709
|
): Promise<PartsLoadedData | FragLoadedData | null> {
|
710
|
+
this.fragCurrent = frag;
|
651
711
|
const details = level?.details;
|
652
712
|
if (!this.levels || !details) {
|
653
713
|
throw new Error(
|
@@ -659,7 +719,7 @@ export default class BaseStreamController
|
|
659
719
|
if (frag.encrypted && !frag.decryptdata?.key) {
|
660
720
|
this.log(
|
661
721
|
`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${
|
662
|
-
this.
|
722
|
+
this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'
|
663
723
|
} ${frag.level}`,
|
664
724
|
);
|
665
725
|
this.state = State.KEY_LOADING;
|
@@ -683,8 +743,23 @@ export default class BaseStreamController
|
|
683
743
|
this.keyLoader.loadClear(frag, details.encryptedFragments);
|
684
744
|
}
|
685
745
|
|
746
|
+
const fragPrevious = this.fragPrevious;
|
747
|
+
if (
|
748
|
+
frag.sn !== 'initSegment' &&
|
749
|
+
(!fragPrevious || frag.sn !== fragPrevious.sn)
|
750
|
+
) {
|
751
|
+
const shouldLoadParts = this.shouldLoadParts(level.details, frag.end);
|
752
|
+
if (shouldLoadParts !== this.loadingParts) {
|
753
|
+
this.log(
|
754
|
+
`LL-Part loading ${
|
755
|
+
shouldLoadParts ? 'ON' : 'OFF'
|
756
|
+
} loading sn ${fragPrevious?.sn}->${frag.sn}`,
|
757
|
+
);
|
758
|
+
this.loadingParts = shouldLoadParts;
|
759
|
+
}
|
760
|
+
}
|
686
761
|
targetBufferTime = Math.max(frag.start, targetBufferTime || 0);
|
687
|
-
if (this.
|
762
|
+
if (this.loadingParts && frag.sn !== 'initSegment') {
|
688
763
|
const partList = details.partList;
|
689
764
|
if (partList && progressCallback) {
|
690
765
|
if (targetBufferTime > frag.end && details.fragmentHint) {
|
@@ -699,7 +774,7 @@ export default class BaseStreamController
|
|
699
774
|
} of playlist [${details.startSN}-${
|
700
775
|
details.endSN
|
701
776
|
}] parts [0-${partIndex}-${partList.length - 1}] ${
|
702
|
-
this.
|
777
|
+
this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'
|
703
778
|
}: ${frag.level}, target: ${parseFloat(
|
704
779
|
targetBufferTime.toFixed(3),
|
705
780
|
)}`,
|
@@ -755,10 +830,22 @@ export default class BaseStreamController
|
|
755
830
|
}
|
756
831
|
}
|
757
832
|
|
833
|
+
if (frag.sn !== 'initSegment' && this.loadingParts) {
|
834
|
+
this.log(
|
835
|
+
`LL-Part loading OFF after next part miss @${targetBufferTime.toFixed(
|
836
|
+
2,
|
837
|
+
)}`,
|
838
|
+
);
|
839
|
+
this.loadingParts = false;
|
840
|
+
} else if (!frag.url) {
|
841
|
+
// Selected fragment hint for part but not loading parts
|
842
|
+
return Promise.resolve(null);
|
843
|
+
}
|
844
|
+
|
758
845
|
this.log(
|
759
846
|
`Loading fragment ${frag.sn} cc: ${frag.cc} ${
|
760
847
|
details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''
|
761
|
-
}${this.
|
848
|
+
}${this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'}: ${
|
762
849
|
frag.level
|
763
850
|
}, target: ${parseFloat(targetBufferTime.toFixed(3))}`,
|
764
851
|
);
|
@@ -825,7 +912,7 @@ export default class BaseStreamController
|
|
825
912
|
const loadedPart = partLoadedData.part as Part;
|
826
913
|
this.hls.trigger(Events.FRAG_LOADED, partLoadedData);
|
827
914
|
const nextPart =
|
828
|
-
getPartWith(level, frag.sn as number, part.index + 1) ||
|
915
|
+
getPartWith(level.details, frag.sn as number, part.index + 1) ||
|
829
916
|
findPart(initialPartList, frag.sn as number, part.index + 1);
|
830
917
|
if (nextPart) {
|
831
918
|
loadPart(nextPart);
|
@@ -882,12 +969,50 @@ export default class BaseStreamController
|
|
882
969
|
if (part) {
|
883
970
|
part.stats.parsing.end = now;
|
884
971
|
}
|
972
|
+
// See if part loading should be disabled/enabled based on buffer and playback position.
|
973
|
+
const levelDetails = this.getLevelDetails();
|
974
|
+
const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
|
975
|
+
const shouldLoadParts =
|
976
|
+
loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
|
977
|
+
if (shouldLoadParts !== this.loadingParts) {
|
978
|
+
this.log(
|
979
|
+
`LL-Part loading ${
|
980
|
+
shouldLoadParts ? 'ON' : 'OFF'
|
981
|
+
} after parsing segment ending @${frag.end.toFixed(2)}`,
|
982
|
+
);
|
983
|
+
this.loadingParts = shouldLoadParts;
|
984
|
+
}
|
885
985
|
this.updateLevelTiming(frag, part, level, chunkMeta.partial);
|
886
986
|
}
|
887
987
|
|
988
|
+
private shouldLoadParts(
|
989
|
+
details: LevelDetails | undefined,
|
990
|
+
bufferEnd: number,
|
991
|
+
): boolean {
|
992
|
+
if (this.config.lowLatencyMode) {
|
993
|
+
if (!details) {
|
994
|
+
return this.loadingParts;
|
995
|
+
}
|
996
|
+
if (details?.partList) {
|
997
|
+
// Buffer must be ahead of first part + duration of parts after last segment
|
998
|
+
// and playback must be at or past segment adjacent to part list
|
999
|
+
const firstPart = details.partList[0];
|
1000
|
+
const safePartStart =
|
1001
|
+
firstPart.end + (details.fragmentHint?.duration || 0);
|
1002
|
+
if (
|
1003
|
+
bufferEnd >= safePartStart &&
|
1004
|
+
this.lastCurrentTime > firstPart.start - firstPart.fragment.duration
|
1005
|
+
) {
|
1006
|
+
return true;
|
1007
|
+
}
|
1008
|
+
}
|
1009
|
+
}
|
1010
|
+
return false;
|
1011
|
+
}
|
1012
|
+
|
888
1013
|
protected getCurrentContext(
|
889
1014
|
chunkMeta: ChunkMetadata,
|
890
|
-
): { frag:
|
1015
|
+
): { frag: MediaFragment; part: Part | null; level: Level } | null {
|
891
1016
|
const { levels, fragCurrent } = this;
|
892
1017
|
const { level: levelIndex, sn, part: partIndex } = chunkMeta;
|
893
1018
|
if (!levels?.[levelIndex]) {
|
@@ -897,10 +1022,13 @@ export default class BaseStreamController
|
|
897
1022
|
return null;
|
898
1023
|
}
|
899
1024
|
const level = levels[levelIndex];
|
900
|
-
const
|
1025
|
+
const levelDetails = level.details;
|
1026
|
+
|
1027
|
+
const part =
|
1028
|
+
partIndex > -1 ? getPartWith(levelDetails, sn, partIndex) : null;
|
901
1029
|
const frag = part
|
902
1030
|
? part.fragment
|
903
|
-
: getFragmentWithSN(
|
1031
|
+
: getFragmentWithSN(levelDetails, sn, fragCurrent);
|
904
1032
|
if (!frag) {
|
905
1033
|
return null;
|
906
1034
|
}
|
@@ -986,22 +1114,26 @@ export default class BaseStreamController
|
|
986
1114
|
if (!Number.isFinite(pos)) {
|
987
1115
|
return null;
|
988
1116
|
}
|
989
|
-
|
1117
|
+
const backwardSeek = this.lastCurrentTime > pos;
|
1118
|
+
const maxBufferHole =
|
1119
|
+
backwardSeek || this.media?.paused ? 0 : this.config.maxBufferHole;
|
1120
|
+
return this.getFwdBufferInfoAtPos(bufferable, pos, type, maxBufferHole);
|
990
1121
|
}
|
991
1122
|
|
992
|
-
|
1123
|
+
private getFwdBufferInfoAtPos(
|
993
1124
|
bufferable: Bufferable | null,
|
994
1125
|
pos: number,
|
995
1126
|
type: PlaylistLevelType,
|
1127
|
+
maxBufferHole: number,
|
996
1128
|
): BufferInfo | null {
|
997
|
-
const {
|
998
|
-
config: { maxBufferHole },
|
999
|
-
} = this;
|
1000
1129
|
const bufferInfo = BufferHelper.bufferInfo(bufferable, pos, maxBufferHole);
|
1001
1130
|
// Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos
|
1002
1131
|
if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
|
1003
1132
|
const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
|
1004
|
-
if (
|
1133
|
+
if (
|
1134
|
+
bufferedFragAtPos &&
|
1135
|
+
(bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)
|
1136
|
+
) {
|
1005
1137
|
return BufferHelper.bufferInfo(
|
1006
1138
|
bufferable,
|
1007
1139
|
pos,
|
@@ -1014,7 +1146,7 @@ export default class BaseStreamController
|
|
1014
1146
|
|
1015
1147
|
protected getMaxBufferLength(levelBitrate?: number): number {
|
1016
1148
|
const { config } = this;
|
1017
|
-
let maxBufLen;
|
1149
|
+
let maxBufLen: number;
|
1018
1150
|
if (levelBitrate) {
|
1019
1151
|
maxBufLen = Math.max(
|
1020
1152
|
(8 * config.maxBufferSize) / levelBitrate,
|
@@ -1050,9 +1182,9 @@ export default class BaseStreamController
|
|
1050
1182
|
position: number,
|
1051
1183
|
playlistType: PlaylistLevelType = PlaylistLevelType.MAIN,
|
1052
1184
|
): Fragment | null {
|
1053
|
-
const fragOrPart = this.fragmentTracker
|
1185
|
+
const fragOrPart = this.fragmentTracker?.getAppendedFrag(
|
1054
1186
|
position,
|
1055
|
-
|
1187
|
+
playlistType,
|
1056
1188
|
);
|
1057
1189
|
if (fragOrPart && 'fragment' in fragOrPart) {
|
1058
1190
|
return fragOrPart.fragment;
|
@@ -1074,7 +1206,8 @@ export default class BaseStreamController
|
|
1074
1206
|
// find fragment index, contiguous with end of buffer position
|
1075
1207
|
const { config } = this;
|
1076
1208
|
const start = fragments[0].start;
|
1077
|
-
|
1209
|
+
const canLoadParts = config.lowLatencyMode && !!levelDetails.partList;
|
1210
|
+
let frag: MediaFragment | null = null;
|
1078
1211
|
|
1079
1212
|
if (levelDetails.live) {
|
1080
1213
|
const initialLiveManifestSize = config.initialLiveManifestSize;
|
@@ -1094,6 +1227,10 @@ export default class BaseStreamController
|
|
1094
1227
|
this.startPosition === -1) ||
|
1095
1228
|
pos < start
|
1096
1229
|
) {
|
1230
|
+
if (canLoadParts && !this.loadingParts) {
|
1231
|
+
this.log(`LL-Part loading ON for initial live fragment`);
|
1232
|
+
this.loadingParts = true;
|
1233
|
+
}
|
1097
1234
|
frag = this.getInitialLiveFragment(levelDetails, fragments);
|
1098
1235
|
this.startPosition = this.nextLoadPosition = frag
|
1099
1236
|
? this.hls.liveSyncPosition || frag.start
|
@@ -1106,7 +1243,7 @@ export default class BaseStreamController
|
|
1106
1243
|
|
1107
1244
|
// If we haven't run into any special cases already, just load the fragment most closely matching the requested position
|
1108
1245
|
if (!frag) {
|
1109
|
-
const end =
|
1246
|
+
const end = this.loadingParts
|
1110
1247
|
? levelDetails.partEnd
|
1111
1248
|
: levelDetails.fragmentEnd;
|
1112
1249
|
frag = this.getFragmentAtPosition(pos, end, levelDetails);
|
@@ -1131,34 +1268,35 @@ export default class BaseStreamController
|
|
1131
1268
|
playlistType: PlaylistLevelType,
|
1132
1269
|
maxBufLen: number,
|
1133
1270
|
): Fragment | null {
|
1134
|
-
|
1135
|
-
|
1136
|
-
this.nextLoadPosition,
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
// Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length
|
1145
|
-
const nextbufferInfo = this.getFwdBufferInfoAtPos(
|
1146
|
-
this.mediaBuffer ? this.mediaBuffer : this.media,
|
1147
|
-
bufferInfo.nextStart,
|
1148
|
-
playlistType,
|
1149
|
-
);
|
1150
|
-
if (
|
1151
|
-
nextbufferInfo !== null &&
|
1152
|
-
bufferInfo.len + nextbufferInfo.len >= maxBufLen
|
1153
|
-
) {
|
1154
|
-
// Returning here might result in not finding an audio and video candiate to skip to
|
1155
|
-
this.log(
|
1156
|
-
`buffer full after gaps in "${playlistType}" playlist starting at sn: ${frag.sn}`,
|
1271
|
+
let nextFragment: Fragment | null = null;
|
1272
|
+
if (frag.gap) {
|
1273
|
+
nextFragment = this.getNextFragment(this.nextLoadPosition, levelDetails);
|
1274
|
+
if (nextFragment && !nextFragment.gap && bufferInfo.nextStart) {
|
1275
|
+
// Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length
|
1276
|
+
const nextbufferInfo = this.getFwdBufferInfoAtPos(
|
1277
|
+
this.mediaBuffer ? this.mediaBuffer : this.media,
|
1278
|
+
bufferInfo.nextStart,
|
1279
|
+
playlistType,
|
1280
|
+
0,
|
1157
1281
|
);
|
1158
|
-
|
1282
|
+
if (
|
1283
|
+
nextbufferInfo !== null &&
|
1284
|
+
bufferInfo.len + nextbufferInfo.len >= maxBufLen
|
1285
|
+
) {
|
1286
|
+
// Returning here might result in not finding an audio and video candiate to skip to
|
1287
|
+
const sn = nextFragment.sn;
|
1288
|
+
if (this.loopSn !== sn) {
|
1289
|
+
this.log(
|
1290
|
+
`buffer full after gaps in "${playlistType}" playlist starting at sn: ${sn}`,
|
1291
|
+
);
|
1292
|
+
this.loopSn = sn;
|
1293
|
+
}
|
1294
|
+
return null;
|
1295
|
+
}
|
1159
1296
|
}
|
1160
1297
|
}
|
1161
|
-
|
1298
|
+
this.loopSn = undefined;
|
1299
|
+
return nextFragment;
|
1162
1300
|
}
|
1163
1301
|
|
1164
1302
|
mapToInitFragWhenRequired(frag: Fragment | null): typeof frag {
|
@@ -1213,10 +1351,10 @@ export default class BaseStreamController
|
|
1213
1351
|
*/
|
1214
1352
|
protected getInitialLiveFragment(
|
1215
1353
|
levelDetails: LevelDetails,
|
1216
|
-
fragments:
|
1217
|
-
):
|
1354
|
+
fragments: MediaFragment[],
|
1355
|
+
): MediaFragment | null {
|
1218
1356
|
const fragPrevious = this.fragPrevious;
|
1219
|
-
let frag:
|
1357
|
+
let frag: MediaFragment | null = null;
|
1220
1358
|
if (fragPrevious) {
|
1221
1359
|
if (levelDetails.hasProgramDateTime) {
|
1222
1360
|
// Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding
|
@@ -1280,7 +1418,7 @@ export default class BaseStreamController
|
|
1280
1418
|
bufferEnd: number,
|
1281
1419
|
end: number,
|
1282
1420
|
levelDetails: LevelDetails,
|
1283
|
-
):
|
1421
|
+
): MediaFragment | null {
|
1284
1422
|
const { config } = this;
|
1285
1423
|
let { fragPrevious } = this;
|
1286
1424
|
let { fragments, endSN } = levelDetails;
|
@@ -1289,20 +1427,25 @@ export default class BaseStreamController
|
|
1289
1427
|
const partList = levelDetails.partList;
|
1290
1428
|
|
1291
1429
|
const loadingParts = !!(
|
1292
|
-
|
1430
|
+
this.loadingParts &&
|
1293
1431
|
partList?.length &&
|
1294
1432
|
fragmentHint
|
1295
1433
|
);
|
1296
1434
|
if (loadingParts && fragmentHint && !this.bitrateTest) {
|
1297
1435
|
// Include incomplete fragment with parts at end
|
1298
1436
|
fragments = fragments.concat(fragmentHint);
|
1299
|
-
endSN = fragmentHint.sn
|
1437
|
+
endSN = fragmentHint.sn;
|
1300
1438
|
}
|
1301
1439
|
|
1302
|
-
let frag;
|
1440
|
+
let frag: MediaFragment | null;
|
1303
1441
|
if (bufferEnd < end) {
|
1442
|
+
const backwardSeek = bufferEnd < this.lastCurrentTime;
|
1304
1443
|
const lookupTolerance =
|
1305
|
-
|
1444
|
+
backwardSeek ||
|
1445
|
+
bufferEnd > end - maxFragLookUpTolerance ||
|
1446
|
+
this.media?.paused
|
1447
|
+
? 0
|
1448
|
+
: maxFragLookUpTolerance;
|
1306
1449
|
// Remove the tolerance if it would put the bufferEnd past the actual end of stream
|
1307
1450
|
// Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE)
|
1308
1451
|
frag = findFragmentByPTS(
|
@@ -1455,7 +1598,7 @@ export default class BaseStreamController
|
|
1455
1598
|
if (startTimeOffset !== null && Number.isFinite(startTimeOffset)) {
|
1456
1599
|
startPosition = sliding + startTimeOffset;
|
1457
1600
|
if (startTimeOffset < 0) {
|
1458
|
-
startPosition += details.
|
1601
|
+
startPosition += details.edge;
|
1459
1602
|
}
|
1460
1603
|
startPosition = Math.min(
|
1461
1604
|
Math.max(sliding, startPosition),
|
@@ -1536,7 +1679,7 @@ export default class BaseStreamController
|
|
1536
1679
|
}
|
1537
1680
|
const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP;
|
1538
1681
|
if (gapTagEncountered) {
|
1539
|
-
this.fragmentTracker.fragBuffered(frag, true);
|
1682
|
+
this.fragmentTracker.fragBuffered(frag as MediaFragment, true);
|
1540
1683
|
}
|
1541
1684
|
// keep retrying until the limit will be reached
|
1542
1685
|
const errorAction = data.errorAction;
|
@@ -1569,7 +1712,7 @@ export default class BaseStreamController
|
|
1569
1712
|
errorAction.resolved = true;
|
1570
1713
|
}
|
1571
1714
|
} else {
|
1572
|
-
|
1715
|
+
this.warn(
|
1573
1716
|
`${data.details} reached or exceeded max retry (${retryCount})`,
|
1574
1717
|
);
|
1575
1718
|
return;
|
@@ -1659,7 +1802,9 @@ export default class BaseStreamController
|
|
1659
1802
|
this.log('Reset loading state');
|
1660
1803
|
this.fragCurrent = null;
|
1661
1804
|
this.fragPrevious = null;
|
1662
|
-
this.state
|
1805
|
+
if (this.state !== State.STOPPED) {
|
1806
|
+
this.state = State.IDLE;
|
1807
|
+
}
|
1663
1808
|
}
|
1664
1809
|
|
1665
1810
|
protected resetStartWhenNotLoaded(level: Level | null): void {
|
@@ -1699,7 +1844,7 @@ export default class BaseStreamController
|
|
1699
1844
|
}
|
1700
1845
|
|
1701
1846
|
private updateLevelTiming(
|
1702
|
-
frag:
|
1847
|
+
frag: MediaFragment,
|
1703
1848
|
part: Part | null,
|
1704
1849
|
level: Level,
|
1705
1850
|
partial: boolean,
|