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.
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 +4211 -2666
  5. package/dist/hls.js.d.ts +179 -110
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2841 -1921
  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 +2569 -1639
  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 +3572 -2017
  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 +156 -136
  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 +234 -89
  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 +84 -47
  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 +32 -62
  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
+ 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
- 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(
@@ -282,7 +314,27 @@ export default class BaseStreamController
282
314
  true,
283
315
  );
284
316
 
285
- this.lastCurrentTime = currentTime;
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 === 'AES-128'
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 = State.IDLE;
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.logPrefix === '[stream-controller]' ? 'level' : 'track'
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.config.lowLatencyMode && frag.sn !== 'initSegment') {
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.logPrefix === '[stream-controller]' ? 'level' : 'track'
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.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${
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: Fragment; part: Part | null; level: Level } | null {
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 part = partIndex > -1 ? getPartWith(level, sn, partIndex) : null;
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(level, sn, fragCurrent);
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
- return this.getFwdBufferInfoAtPos(bufferable, pos, type);
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
- protected getFwdBufferInfoAtPos(
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 (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) {
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.getAppendedFrag(
1185
+ const fragOrPart = this.fragmentTracker?.getAppendedFrag(
1054
1186
  position,
1055
- PlaylistLevelType.MAIN,
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
- let frag;
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 = config.lowLatencyMode
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
- const gapStart = frag.gap;
1135
- const nextFragment = this.getNextFragment(
1136
- this.nextLoadPosition,
1137
- levelDetails,
1138
- );
1139
- if (nextFragment === null) {
1140
- return nextFragment;
1141
- }
1142
- frag = nextFragment;
1143
- if (gapStart && frag && !frag.gap && bufferInfo.nextStart) {
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
- return null;
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
- return frag;
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: Array<Fragment>,
1217
- ): Fragment | null {
1354
+ fragments: MediaFragment[],
1355
+ ): MediaFragment | null {
1218
1356
  const fragPrevious = this.fragPrevious;
1219
- let frag: Fragment | null = null;
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
- ): Fragment | null {
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
- config.lowLatencyMode &&
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 as number;
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
- bufferEnd > end - maxFragLookUpTolerance ? 0 : maxFragLookUpTolerance;
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.totalduration;
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
- logger.warn(
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 = State.IDLE;
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: Fragment,
1847
+ frag: MediaFragment,
1703
1848
  part: Part | null,
1704
1849
  level: Level,
1705
1850
  partial: boolean,