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.
Files changed (103) hide show
  1. package/README.md +4 -3
  2. package/dist/hls-demo.js +41 -38
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +4174 -2625
  5. package/dist/hls.js.d.ts +173 -108
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2851 -1914
  8. package/dist/hls.light.js.map +1 -1
  9. package/dist/hls.light.min.js +1 -1
  10. package/dist/hls.light.min.js.map +1 -1
  11. package/dist/hls.light.mjs +2560 -1608
  12. package/dist/hls.light.mjs.map +1 -1
  13. package/dist/hls.min.js +1 -1
  14. package/dist/hls.min.js.map +1 -1
  15. package/dist/hls.mjs +3546 -1982
  16. package/dist/hls.mjs.map +1 -1
  17. package/dist/hls.worker.js +1 -1
  18. package/dist/hls.worker.js.map +1 -1
  19. package/package.json +38 -38
  20. package/src/config.ts +5 -2
  21. package/src/controller/abr-controller.ts +39 -25
  22. package/src/controller/audio-stream-controller.ts +141 -137
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +27 -10
  25. package/src/controller/base-stream-controller.ts +215 -82
  26. package/src/controller/buffer-controller.ts +250 -97
  27. package/src/controller/buffer-operation-queue.ts +16 -19
  28. package/src/controller/cap-level-controller.ts +3 -2
  29. package/src/controller/cmcd-controller.ts +51 -14
  30. package/src/controller/content-steering-controller.ts +29 -15
  31. package/src/controller/eme-controller.ts +10 -23
  32. package/src/controller/error-controller.ts +28 -22
  33. package/src/controller/fps-controller.ts +8 -3
  34. package/src/controller/fragment-finders.ts +44 -16
  35. package/src/controller/fragment-tracker.ts +58 -25
  36. package/src/controller/gap-controller.ts +43 -16
  37. package/src/controller/id3-track-controller.ts +45 -35
  38. package/src/controller/latency-controller.ts +18 -13
  39. package/src/controller/level-controller.ts +37 -19
  40. package/src/controller/stream-controller.ts +100 -83
  41. package/src/controller/subtitle-stream-controller.ts +35 -47
  42. package/src/controller/subtitle-track-controller.ts +5 -3
  43. package/src/controller/timeline-controller.ts +20 -22
  44. package/src/crypt/aes-crypto.ts +21 -2
  45. package/src/crypt/decrypter-aes-mode.ts +4 -0
  46. package/src/crypt/decrypter.ts +32 -16
  47. package/src/crypt/fast-aes-key.ts +28 -5
  48. package/src/demux/audio/aacdemuxer.ts +2 -2
  49. package/src/demux/audio/ac3-demuxer.ts +4 -3
  50. package/src/demux/audio/adts.ts +9 -4
  51. package/src/demux/audio/base-audio-demuxer.ts +16 -14
  52. package/src/demux/audio/mp3demuxer.ts +4 -3
  53. package/src/demux/audio/mpegaudio.ts +1 -1
  54. package/src/demux/mp4demuxer.ts +7 -7
  55. package/src/demux/sample-aes.ts +2 -0
  56. package/src/demux/transmuxer-interface.ts +8 -16
  57. package/src/demux/transmuxer-worker.ts +4 -4
  58. package/src/demux/transmuxer.ts +16 -3
  59. package/src/demux/tsdemuxer.ts +75 -38
  60. package/src/demux/video/avc-video-parser.ts +210 -121
  61. package/src/demux/video/base-video-parser.ts +135 -2
  62. package/src/demux/video/exp-golomb.ts +0 -208
  63. package/src/demux/video/hevc-video-parser.ts +749 -0
  64. package/src/events.ts +8 -1
  65. package/src/exports-named.ts +1 -1
  66. package/src/hls.ts +73 -43
  67. package/src/loader/date-range.ts +71 -5
  68. package/src/loader/fragment-loader.ts +23 -21
  69. package/src/loader/fragment.ts +8 -4
  70. package/src/loader/key-loader.ts +3 -1
  71. package/src/loader/level-details.ts +6 -6
  72. package/src/loader/level-key.ts +10 -9
  73. package/src/loader/m3u8-parser.ts +138 -144
  74. package/src/loader/playlist-loader.ts +5 -7
  75. package/src/remux/mp4-generator.ts +196 -1
  76. package/src/remux/mp4-remuxer.ts +36 -16
  77. package/src/remux/passthrough-remuxer.ts +1 -1
  78. package/src/task-loop.ts +5 -2
  79. package/src/types/component-api.ts +3 -1
  80. package/src/types/demuxer.ts +3 -0
  81. package/src/types/events.ts +19 -6
  82. package/src/types/fragment-tracker.ts +2 -2
  83. package/src/types/media-playlist.ts +9 -1
  84. package/src/types/remuxer.ts +1 -1
  85. package/src/utils/attr-list.ts +96 -9
  86. package/src/utils/buffer-helper.ts +12 -31
  87. package/src/utils/cea-608-parser.ts +1 -3
  88. package/src/utils/codecs.ts +34 -5
  89. package/src/utils/encryption-methods-util.ts +21 -0
  90. package/src/utils/fetch-loader.ts +1 -1
  91. package/src/utils/hash.ts +10 -0
  92. package/src/utils/hdr.ts +4 -7
  93. package/src/utils/imsc1-ttml-parser.ts +1 -1
  94. package/src/utils/keysystem-util.ts +1 -6
  95. package/src/utils/level-helper.ts +71 -44
  96. package/src/utils/logger.ts +58 -23
  97. package/src/utils/mp4-tools.ts +5 -3
  98. package/src/utils/rendition-helper.ts +100 -74
  99. package/src/utils/utf8-utils.ts +18 -0
  100. package/src/utils/variable-substitution.ts +0 -19
  101. package/src/utils/webvtt-parser.ts +2 -12
  102. package/src/demux/id3.ts +0 -411
  103. 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: Fragment | null = null;
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 onvseeking: EventListener | null = null;
101
- protected onvended: EventListener | null = null;
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
- this.onvseeking = this.onMediaSeeking.bind(this) as EventListener;
201
- this.onvended = this.onMediaEnded.bind(this) as EventListener;
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 && this.onvseeking && this.onvended) {
219
- media.removeEventListener('seeking', this.onvseeking);
220
- media.removeEventListener('ended', this.onvended);
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.loadedmetadata = false;
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 onMediaSeeking() {
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 === 'AES-128'
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 = State.IDLE;
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.logPrefix === '[stream-controller]' ? 'level' : 'track'
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.config.lowLatencyMode && frag.sn !== 'initSegment') {
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.logPrefix === '[stream-controller]' ? 'level' : 'track'
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.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${
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: Fragment; part: Part | null; level: Level } | null {
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 part = partIndex > -1 ? getPartWith(level, sn, partIndex) : null;
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(level, sn, fragCurrent);
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 (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) {
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.getAppendedFrag(
1178
+ const fragOrPart = this.fragmentTracker?.getAppendedFrag(
1053
1179
  position,
1054
- PlaylistLevelType.MAIN,
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
- let frag;
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 = config.lowLatencyMode
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
- const gapStart = frag.gap;
1134
- const nextFragment = this.getNextFragment(
1135
- this.nextLoadPosition,
1136
- levelDetails,
1137
- );
1138
- if (nextFragment === null) {
1139
- return nextFragment;
1140
- }
1141
- frag = nextFragment;
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
- return null;
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
- return frag;
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: Array<Fragment>,
1216
- ): Fragment | null {
1346
+ fragments: MediaFragment[],
1347
+ ): MediaFragment | null {
1217
1348
  const fragPrevious = this.fragPrevious;
1218
- let frag: Fragment | null = null;
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
- ): Fragment | null {
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
- config.lowLatencyMode &&
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 as number;
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.totalduration;
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
- logger.warn(
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 = State.IDLE;
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: Fragment,
1834
+ frag: MediaFragment,
1702
1835
  part: Part | null,
1703
1836
  level: Level,
1704
1837
  partial: boolean,