hls.js 1.5.12 → 1.5.13-0.canary.10401
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 +4174 -2625
- package/dist/hls.js.d.ts +173 -108
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +2851 -1914
- 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 +2560 -1608
- 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 +3546 -1982
- 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 +141 -137
- 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 +215 -82
- 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 +73 -43
- 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 +36 -16
- 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
|
-
|
102
|
-
|
103
|
-
private readonly logPrefix: string = '';
|
104
|
-
protected log: (msg: any) => void;
|
105
|
-
protected warn: (msg: any) => void;
|
103
|
+
protected buffering: boolean = true;
|
104
|
+
private 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(
|
@@ -283,6 +315,21 @@ export default class BaseStreamController
|
|
283
315
|
);
|
284
316
|
|
285
317
|
this.lastCurrentTime = currentTime;
|
318
|
+
if (!this.loadingParts) {
|
319
|
+
const bufferEnd = Math.max(bufferInfo.end, currentTime);
|
320
|
+
const shouldLoadParts = this.shouldLoadParts(
|
321
|
+
this.getLevelDetails(),
|
322
|
+
bufferEnd,
|
323
|
+
);
|
324
|
+
if (shouldLoadParts) {
|
325
|
+
this.log(
|
326
|
+
`LL-Part loading ON after seeking to ${currentTime.toFixed(
|
327
|
+
2,
|
328
|
+
)} with buffer @${bufferEnd.toFixed(2)}`,
|
329
|
+
);
|
330
|
+
this.loadingParts = shouldLoadParts;
|
331
|
+
}
|
332
|
+
}
|
286
333
|
}
|
287
334
|
|
288
335
|
// in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
|
@@ -292,27 +339,30 @@ export default class BaseStreamController
|
|
292
339
|
|
293
340
|
// Async tick to speed up processing
|
294
341
|
this.tickImmediate();
|
295
|
-
}
|
342
|
+
};
|
296
343
|
|
297
|
-
protected onMediaEnded() {
|
344
|
+
protected onMediaEnded = () => {
|
298
345
|
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
|
299
346
|
this.startPosition = this.lastCurrentTime = 0;
|
300
|
-
|
347
|
+
if (this.playlistType === PlaylistLevelType.MAIN) {
|
348
|
+
this.hls.trigger(Events.MEDIA_ENDED, {
|
349
|
+
stalled: false,
|
350
|
+
});
|
351
|
+
}
|
352
|
+
};
|
301
353
|
|
302
354
|
protected onManifestLoaded(
|
303
355
|
event: Events.MANIFEST_LOADED,
|
304
356
|
data: ManifestLoadedData,
|
305
357
|
): void {
|
306
358
|
this.startTimeOffset = data.startTimeOffset;
|
307
|
-
this.initPTS = [];
|
308
359
|
}
|
309
360
|
|
310
361
|
protected onHandlerDestroying() {
|
311
|
-
this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
312
362
|
this.stopLoad();
|
313
363
|
super.onHandlerDestroying();
|
314
364
|
// @ts-ignore
|
315
|
-
this.hls = null;
|
365
|
+
this.hls = this.onMediaSeeking = this.onMediaEnded = null;
|
316
366
|
}
|
317
367
|
|
318
368
|
protected onHandlerDestroyed() {
|
@@ -343,6 +393,7 @@ export default class BaseStreamController
|
|
343
393
|
level: Level,
|
344
394
|
targetBufferTime: number,
|
345
395
|
) {
|
396
|
+
this.startFragRequested = true;
|
346
397
|
this._loadFragForPlayback(frag, level, targetBufferTime);
|
347
398
|
}
|
348
399
|
|
@@ -496,7 +547,7 @@ export default class BaseStreamController
|
|
496
547
|
payload.byteLength > 0 &&
|
497
548
|
decryptData?.key &&
|
498
549
|
decryptData.iv &&
|
499
|
-
decryptData.method
|
550
|
+
isFullSegmentEncryption(decryptData.method)
|
500
551
|
) {
|
501
552
|
const startTime = self.performance.now();
|
502
553
|
// decrypt init segment data
|
@@ -505,6 +556,7 @@ export default class BaseStreamController
|
|
505
556
|
new Uint8Array(payload),
|
506
557
|
decryptData.key.buffer,
|
507
558
|
decryptData.iv.buffer,
|
559
|
+
getAesModeFromFullSegmentMethod(decryptData.method),
|
508
560
|
)
|
509
561
|
.catch((err) => {
|
510
562
|
hls.trigger(Events.ERROR, {
|
@@ -548,7 +600,9 @@ export default class BaseStreamController
|
|
548
600
|
throw new Error('init load aborted, missing levels');
|
549
601
|
}
|
550
602
|
const stats = data.frag.stats;
|
551
|
-
this.state
|
603
|
+
if (this.state !== State.STOPPED) {
|
604
|
+
this.state = State.IDLE;
|
605
|
+
}
|
552
606
|
data.frag.data = new Uint8Array(data.payload);
|
553
607
|
stats.parsing.start = stats.buffering.start = self.performance.now();
|
554
608
|
stats.parsing.end = stats.buffering.end = self.performance.now();
|
@@ -648,6 +702,7 @@ export default class BaseStreamController
|
|
648
702
|
targetBufferTime: number | null = null,
|
649
703
|
progressCallback?: FragmentLoadProgressCallback,
|
650
704
|
): Promise<PartsLoadedData | FragLoadedData | null> {
|
705
|
+
this.fragCurrent = frag;
|
651
706
|
const details = level?.details;
|
652
707
|
if (!this.levels || !details) {
|
653
708
|
throw new Error(
|
@@ -659,7 +714,7 @@ export default class BaseStreamController
|
|
659
714
|
if (frag.encrypted && !frag.decryptdata?.key) {
|
660
715
|
this.log(
|
661
716
|
`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${
|
662
|
-
this.
|
717
|
+
this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'
|
663
718
|
} ${frag.level}`,
|
664
719
|
);
|
665
720
|
this.state = State.KEY_LOADING;
|
@@ -683,8 +738,23 @@ export default class BaseStreamController
|
|
683
738
|
this.keyLoader.loadClear(frag, details.encryptedFragments);
|
684
739
|
}
|
685
740
|
|
741
|
+
const fragPrevious = this.fragPrevious;
|
742
|
+
if (
|
743
|
+
frag.sn !== 'initSegment' &&
|
744
|
+
(!fragPrevious || frag.sn !== fragPrevious.sn)
|
745
|
+
) {
|
746
|
+
const shouldLoadParts = this.shouldLoadParts(level.details, frag.end);
|
747
|
+
if (shouldLoadParts !== this.loadingParts) {
|
748
|
+
this.log(
|
749
|
+
`LL-Part loading ${
|
750
|
+
shouldLoadParts ? 'ON' : 'OFF'
|
751
|
+
} loading sn ${fragPrevious?.sn}->${frag.sn}`,
|
752
|
+
);
|
753
|
+
this.loadingParts = shouldLoadParts;
|
754
|
+
}
|
755
|
+
}
|
686
756
|
targetBufferTime = Math.max(frag.start, targetBufferTime || 0);
|
687
|
-
if (this.
|
757
|
+
if (this.loadingParts && frag.sn !== 'initSegment') {
|
688
758
|
const partList = details.partList;
|
689
759
|
if (partList && progressCallback) {
|
690
760
|
if (targetBufferTime > frag.end && details.fragmentHint) {
|
@@ -699,7 +769,7 @@ export default class BaseStreamController
|
|
699
769
|
} of playlist [${details.startSN}-${
|
700
770
|
details.endSN
|
701
771
|
}] parts [0-${partIndex}-${partList.length - 1}] ${
|
702
|
-
this.
|
772
|
+
this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'
|
703
773
|
}: ${frag.level}, target: ${parseFloat(
|
704
774
|
targetBufferTime.toFixed(3),
|
705
775
|
)}`,
|
@@ -755,10 +825,22 @@ export default class BaseStreamController
|
|
755
825
|
}
|
756
826
|
}
|
757
827
|
|
828
|
+
if (frag.sn !== 'initSegment' && this.loadingParts) {
|
829
|
+
this.log(
|
830
|
+
`LL-Part loading OFF after next part miss @${targetBufferTime.toFixed(
|
831
|
+
2,
|
832
|
+
)}`,
|
833
|
+
);
|
834
|
+
this.loadingParts = false;
|
835
|
+
} else if (!frag.url) {
|
836
|
+
// Selected fragment hint for part but not loading parts
|
837
|
+
return Promise.resolve(null);
|
838
|
+
}
|
839
|
+
|
758
840
|
this.log(
|
759
841
|
`Loading fragment ${frag.sn} cc: ${frag.cc} ${
|
760
842
|
details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''
|
761
|
-
}${this.
|
843
|
+
}${this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'}: ${
|
762
844
|
frag.level
|
763
845
|
}, target: ${parseFloat(targetBufferTime.toFixed(3))}`,
|
764
846
|
);
|
@@ -825,7 +907,7 @@ export default class BaseStreamController
|
|
825
907
|
const loadedPart = partLoadedData.part as Part;
|
826
908
|
this.hls.trigger(Events.FRAG_LOADED, partLoadedData);
|
827
909
|
const nextPart =
|
828
|
-
getPartWith(level, frag.sn as number, part.index + 1) ||
|
910
|
+
getPartWith(level.details, frag.sn as number, part.index + 1) ||
|
829
911
|
findPart(initialPartList, frag.sn as number, part.index + 1);
|
830
912
|
if (nextPart) {
|
831
913
|
loadPart(nextPart);
|
@@ -882,12 +964,50 @@ export default class BaseStreamController
|
|
882
964
|
if (part) {
|
883
965
|
part.stats.parsing.end = now;
|
884
966
|
}
|
967
|
+
// See if part loading should be disabled/enabled based on buffer and playback position.
|
968
|
+
const levelDetails = this.getLevelDetails();
|
969
|
+
const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
|
970
|
+
const shouldLoadParts =
|
971
|
+
loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
|
972
|
+
if (shouldLoadParts !== this.loadingParts) {
|
973
|
+
this.log(
|
974
|
+
`LL-Part loading ${
|
975
|
+
shouldLoadParts ? 'ON' : 'OFF'
|
976
|
+
} after parsing segment ending @${frag.end.toFixed(2)}`,
|
977
|
+
);
|
978
|
+
this.loadingParts = shouldLoadParts;
|
979
|
+
}
|
885
980
|
this.updateLevelTiming(frag, part, level, chunkMeta.partial);
|
886
981
|
}
|
887
982
|
|
983
|
+
private shouldLoadParts(
|
984
|
+
details: LevelDetails | undefined,
|
985
|
+
bufferEnd: number,
|
986
|
+
): boolean {
|
987
|
+
if (this.config.lowLatencyMode) {
|
988
|
+
if (!details) {
|
989
|
+
return this.loadingParts;
|
990
|
+
}
|
991
|
+
if (details?.partList) {
|
992
|
+
// Buffer must be ahead of first part + duration of parts after last segment
|
993
|
+
// and playback must be at or past segment adjacent to part list
|
994
|
+
const firstPart = details.partList[0];
|
995
|
+
const safePartStart =
|
996
|
+
firstPart.end + (details.fragmentHint?.duration || 0);
|
997
|
+
if (
|
998
|
+
bufferEnd >= safePartStart &&
|
999
|
+
this.lastCurrentTime > firstPart.start - firstPart.fragment.duration
|
1000
|
+
) {
|
1001
|
+
return true;
|
1002
|
+
}
|
1003
|
+
}
|
1004
|
+
}
|
1005
|
+
return false;
|
1006
|
+
}
|
1007
|
+
|
888
1008
|
protected getCurrentContext(
|
889
1009
|
chunkMeta: ChunkMetadata,
|
890
|
-
): { frag:
|
1010
|
+
): { frag: MediaFragment; part: Part | null; level: Level } | null {
|
891
1011
|
const { levels, fragCurrent } = this;
|
892
1012
|
const { level: levelIndex, sn, part: partIndex } = chunkMeta;
|
893
1013
|
if (!levels?.[levelIndex]) {
|
@@ -897,10 +1017,13 @@ export default class BaseStreamController
|
|
897
1017
|
return null;
|
898
1018
|
}
|
899
1019
|
const level = levels[levelIndex];
|
900
|
-
const
|
1020
|
+
const levelDetails = level.details;
|
1021
|
+
|
1022
|
+
const part =
|
1023
|
+
partIndex > -1 ? getPartWith(levelDetails, sn, partIndex) : null;
|
901
1024
|
const frag = part
|
902
1025
|
? part.fragment
|
903
|
-
: getFragmentWithSN(
|
1026
|
+
: getFragmentWithSN(levelDetails, sn, fragCurrent);
|
904
1027
|
if (!frag) {
|
905
1028
|
return null;
|
906
1029
|
}
|
@@ -1001,7 +1124,10 @@ export default class BaseStreamController
|
|
1001
1124
|
// Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos
|
1002
1125
|
if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
|
1003
1126
|
const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
|
1004
|
-
if (
|
1127
|
+
if (
|
1128
|
+
bufferedFragAtPos &&
|
1129
|
+
(bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)
|
1130
|
+
) {
|
1005
1131
|
return BufferHelper.bufferInfo(
|
1006
1132
|
bufferable,
|
1007
1133
|
pos,
|
@@ -1014,7 +1140,7 @@ export default class BaseStreamController
|
|
1014
1140
|
|
1015
1141
|
protected getMaxBufferLength(levelBitrate?: number): number {
|
1016
1142
|
const { config } = this;
|
1017
|
-
let maxBufLen;
|
1143
|
+
let maxBufLen: number;
|
1018
1144
|
if (levelBitrate) {
|
1019
1145
|
maxBufLen = Math.max(
|
1020
1146
|
(8 * config.maxBufferSize) / levelBitrate,
|
@@ -1049,9 +1175,9 @@ export default class BaseStreamController
|
|
1049
1175
|
position: number,
|
1050
1176
|
playlistType: PlaylistLevelType = PlaylistLevelType.MAIN,
|
1051
1177
|
): Fragment | null {
|
1052
|
-
const fragOrPart = this.fragmentTracker
|
1178
|
+
const fragOrPart = this.fragmentTracker?.getAppendedFrag(
|
1053
1179
|
position,
|
1054
|
-
|
1180
|
+
playlistType,
|
1055
1181
|
);
|
1056
1182
|
if (fragOrPart && 'fragment' in fragOrPart) {
|
1057
1183
|
return fragOrPart.fragment;
|
@@ -1073,7 +1199,8 @@ export default class BaseStreamController
|
|
1073
1199
|
// find fragment index, contiguous with end of buffer position
|
1074
1200
|
const { config } = this;
|
1075
1201
|
const start = fragments[0].start;
|
1076
|
-
|
1202
|
+
const canLoadParts = config.lowLatencyMode && !!levelDetails.partList;
|
1203
|
+
let frag: MediaFragment | null = null;
|
1077
1204
|
|
1078
1205
|
if (levelDetails.live) {
|
1079
1206
|
const initialLiveManifestSize = config.initialLiveManifestSize;
|
@@ -1093,6 +1220,10 @@ export default class BaseStreamController
|
|
1093
1220
|
this.startPosition === -1) ||
|
1094
1221
|
pos < start
|
1095
1222
|
) {
|
1223
|
+
if (canLoadParts && !this.loadingParts) {
|
1224
|
+
this.log(`LL-Part loading ON for initial live fragment`);
|
1225
|
+
this.loadingParts = true;
|
1226
|
+
}
|
1096
1227
|
frag = this.getInitialLiveFragment(levelDetails, fragments);
|
1097
1228
|
this.startPosition = this.nextLoadPosition = frag
|
1098
1229
|
? this.hls.liveSyncPosition || frag.start
|
@@ -1105,7 +1236,7 @@ export default class BaseStreamController
|
|
1105
1236
|
|
1106
1237
|
// If we haven't run into any special cases already, just load the fragment most closely matching the requested position
|
1107
1238
|
if (!frag) {
|
1108
|
-
const end =
|
1239
|
+
const end = this.loadingParts
|
1109
1240
|
? levelDetails.partEnd
|
1110
1241
|
: levelDetails.fragmentEnd;
|
1111
1242
|
frag = this.getFragmentAtPosition(pos, end, levelDetails);
|
@@ -1130,34 +1261,34 @@ export default class BaseStreamController
|
|
1130
1261
|
playlistType: PlaylistLevelType,
|
1131
1262
|
maxBufLen: number,
|
1132
1263
|
): Fragment | null {
|
1133
|
-
|
1134
|
-
|
1135
|
-
this.nextLoadPosition,
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
if (gapStart && frag && !frag.gap && bufferInfo.nextStart) {
|
1143
|
-
// Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length
|
1144
|
-
const nextbufferInfo = this.getFwdBufferInfoAtPos(
|
1145
|
-
this.mediaBuffer ? this.mediaBuffer : this.media,
|
1146
|
-
bufferInfo.nextStart,
|
1147
|
-
playlistType,
|
1148
|
-
);
|
1149
|
-
if (
|
1150
|
-
nextbufferInfo !== null &&
|
1151
|
-
bufferInfo.len + nextbufferInfo.len >= maxBufLen
|
1152
|
-
) {
|
1153
|
-
// Returning here might result in not finding an audio and video candiate to skip to
|
1154
|
-
this.log(
|
1155
|
-
`buffer full after gaps in "${playlistType}" playlist starting at sn: ${frag.sn}`,
|
1264
|
+
let nextFragment: Fragment | null = null;
|
1265
|
+
if (frag.gap) {
|
1266
|
+
nextFragment = this.getNextFragment(this.nextLoadPosition, levelDetails);
|
1267
|
+
if (nextFragment && !nextFragment.gap && bufferInfo.nextStart) {
|
1268
|
+
// Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length
|
1269
|
+
const nextbufferInfo = this.getFwdBufferInfoAtPos(
|
1270
|
+
this.mediaBuffer ? this.mediaBuffer : this.media,
|
1271
|
+
bufferInfo.nextStart,
|
1272
|
+
playlistType,
|
1156
1273
|
);
|
1157
|
-
|
1274
|
+
if (
|
1275
|
+
nextbufferInfo !== null &&
|
1276
|
+
bufferInfo.len + nextbufferInfo.len >= maxBufLen
|
1277
|
+
) {
|
1278
|
+
// Returning here might result in not finding an audio and video candiate to skip to
|
1279
|
+
const sn = nextFragment.sn;
|
1280
|
+
if (this.loopSn !== sn) {
|
1281
|
+
this.log(
|
1282
|
+
`buffer full after gaps in "${playlistType}" playlist starting at sn: ${sn}`,
|
1283
|
+
);
|
1284
|
+
this.loopSn = sn;
|
1285
|
+
}
|
1286
|
+
return null;
|
1287
|
+
}
|
1158
1288
|
}
|
1159
1289
|
}
|
1160
|
-
|
1290
|
+
this.loopSn = undefined;
|
1291
|
+
return nextFragment;
|
1161
1292
|
}
|
1162
1293
|
|
1163
1294
|
mapToInitFragWhenRequired(frag: Fragment | null): typeof frag {
|
@@ -1212,10 +1343,10 @@ export default class BaseStreamController
|
|
1212
1343
|
*/
|
1213
1344
|
protected getInitialLiveFragment(
|
1214
1345
|
levelDetails: LevelDetails,
|
1215
|
-
fragments:
|
1216
|
-
):
|
1346
|
+
fragments: MediaFragment[],
|
1347
|
+
): MediaFragment | null {
|
1217
1348
|
const fragPrevious = this.fragPrevious;
|
1218
|
-
let frag:
|
1349
|
+
let frag: MediaFragment | null = null;
|
1219
1350
|
if (fragPrevious) {
|
1220
1351
|
if (levelDetails.hasProgramDateTime) {
|
1221
1352
|
// Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding
|
@@ -1279,7 +1410,7 @@ export default class BaseStreamController
|
|
1279
1410
|
bufferEnd: number,
|
1280
1411
|
end: number,
|
1281
1412
|
levelDetails: LevelDetails,
|
1282
|
-
):
|
1413
|
+
): MediaFragment | null {
|
1283
1414
|
const { config } = this;
|
1284
1415
|
let { fragPrevious } = this;
|
1285
1416
|
let { fragments, endSN } = levelDetails;
|
@@ -1288,17 +1419,17 @@ export default class BaseStreamController
|
|
1288
1419
|
const partList = levelDetails.partList;
|
1289
1420
|
|
1290
1421
|
const loadingParts = !!(
|
1291
|
-
|
1422
|
+
this.loadingParts &&
|
1292
1423
|
partList?.length &&
|
1293
1424
|
fragmentHint
|
1294
1425
|
);
|
1295
1426
|
if (loadingParts && fragmentHint && !this.bitrateTest) {
|
1296
1427
|
// Include incomplete fragment with parts at end
|
1297
1428
|
fragments = fragments.concat(fragmentHint);
|
1298
|
-
endSN = fragmentHint.sn
|
1429
|
+
endSN = fragmentHint.sn;
|
1299
1430
|
}
|
1300
1431
|
|
1301
|
-
let frag;
|
1432
|
+
let frag: MediaFragment | null;
|
1302
1433
|
if (bufferEnd < end) {
|
1303
1434
|
const lookupTolerance =
|
1304
1435
|
bufferEnd > end - maxFragLookUpTolerance ? 0 : maxFragLookUpTolerance;
|
@@ -1454,7 +1585,7 @@ export default class BaseStreamController
|
|
1454
1585
|
if (startTimeOffset !== null && Number.isFinite(startTimeOffset)) {
|
1455
1586
|
startPosition = sliding + startTimeOffset;
|
1456
1587
|
if (startTimeOffset < 0) {
|
1457
|
-
startPosition += details.
|
1588
|
+
startPosition += details.edge;
|
1458
1589
|
}
|
1459
1590
|
startPosition = Math.min(
|
1460
1591
|
Math.max(sliding, startPosition),
|
@@ -1535,7 +1666,7 @@ export default class BaseStreamController
|
|
1535
1666
|
}
|
1536
1667
|
const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP;
|
1537
1668
|
if (gapTagEncountered) {
|
1538
|
-
this.fragmentTracker.fragBuffered(frag, true);
|
1669
|
+
this.fragmentTracker.fragBuffered(frag as MediaFragment, true);
|
1539
1670
|
}
|
1540
1671
|
// keep retrying until the limit will be reached
|
1541
1672
|
const errorAction = data.errorAction;
|
@@ -1568,7 +1699,7 @@ export default class BaseStreamController
|
|
1568
1699
|
errorAction.resolved = true;
|
1569
1700
|
}
|
1570
1701
|
} else {
|
1571
|
-
|
1702
|
+
this.warn(
|
1572
1703
|
`${data.details} reached or exceeded max retry (${retryCount})`,
|
1573
1704
|
);
|
1574
1705
|
return;
|
@@ -1658,7 +1789,9 @@ export default class BaseStreamController
|
|
1658
1789
|
this.log('Reset loading state');
|
1659
1790
|
this.fragCurrent = null;
|
1660
1791
|
this.fragPrevious = null;
|
1661
|
-
this.state
|
1792
|
+
if (this.state !== State.STOPPED) {
|
1793
|
+
this.state = State.IDLE;
|
1794
|
+
}
|
1662
1795
|
}
|
1663
1796
|
|
1664
1797
|
protected resetStartWhenNotLoaded(level: Level | null): void {
|
@@ -1698,7 +1831,7 @@ export default class BaseStreamController
|
|
1698
1831
|
}
|
1699
1832
|
|
1700
1833
|
private updateLevelTiming(
|
1701
|
-
frag:
|
1834
|
+
frag: MediaFragment,
|
1702
1835
|
part: Part | null,
|
1703
1836
|
level: Level,
|
1704
1837
|
partial: boolean,
|