hls.js 1.5.7-0.canary.10015 → 1.5.7-0.canary.10017

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/package.json CHANGED
@@ -130,5 +130,5 @@
130
130
  "url-toolkit": "2.2.5",
131
131
  "wrangler": "3.28.2"
132
132
  },
133
- "version": "1.5.7-0.canary.10015"
133
+ "version": "1.5.7-0.canary.10017"
134
134
  }
@@ -539,6 +539,9 @@ class AbrController extends Logger implements AbrComponentAPI {
539
539
 
540
540
  private getNextABRAutoLevel(): number {
541
541
  const { fragCurrent, partCurrent, hls } = this;
542
+ if (hls.levels.length <= 1) {
543
+ return hls.loadLevel;
544
+ }
542
545
  const { maxAutoLevel, config, minAutoLevel } = hls;
543
546
  const currentFragDuration = partCurrent
544
547
  ? partCurrent.duration
@@ -329,12 +329,8 @@ class AudioStreamController
329
329
  return;
330
330
  }
331
331
 
332
- const mainBufferInfo = this.getFwdBufferInfo(
333
- this.videoBuffer ? this.videoBuffer : this.media,
334
- PlaylistLevelType.MAIN,
335
- );
336
332
  const bufferLen = bufferInfo.len;
337
- const maxBufLen = this.getMaxBufferLength(mainBufferInfo?.len);
333
+ const maxBufLen = hls.maxBufferLength;
338
334
 
339
335
  const fragments = trackDetails.fragments;
340
336
  const start = fragments[0].start;
@@ -390,45 +386,37 @@ class AudioStreamController
390
386
  return;
391
387
  }
392
388
 
393
- // Buffer audio up to one target duration ahead of main buffer
394
- const atBufferSyncLimit =
395
- mainBufferInfo &&
396
- frag.start > mainBufferInfo.end + trackDetails.targetduration;
397
- if (
398
- atBufferSyncLimit ||
399
- // Or wait for main buffer after buffing some audio
400
- (!mainBufferInfo?.len && bufferInfo.len)
401
- ) {
402
- // Check fragment-tracker for main fragments since GAP segments do not show up in bufferInfo
403
- const mainFrag = this.getAppendedFrag(frag.start, PlaylistLevelType.MAIN);
404
- if (mainFrag === null) {
405
- return;
406
- }
407
- // Bridge gaps in main buffer
408
- atGap ||=
409
- !!mainFrag.gap || (!!atBufferSyncLimit && mainBufferInfo.len === 0);
410
- if (
411
- (atBufferSyncLimit && !atGap) ||
412
- (atGap && bufferInfo.nextStart && bufferInfo.nextStart < mainFrag.end)
413
- ) {
414
- return;
389
+ if (!trackDetails.live || targetBufferTime < this.hls.liveSyncPosition!) {
390
+ // Request audio segments up to one fragment ahead of main buffer
391
+ const mainBufferInfo = this.getFwdBufferInfo(
392
+ this.videoBuffer ? this.videoBuffer : this.media,
393
+ PlaylistLevelType.MAIN,
394
+ );
395
+ const atBufferSyncLimit =
396
+ !!mainBufferInfo && frag.start > mainBufferInfo.end + frag.duration;
397
+ if (atBufferSyncLimit) {
398
+ // Check fragment-tracker for main fragments since GAP segments do not show up in bufferInfo
399
+ const mainFrag = this.fragmentTracker.getFragAtPos(
400
+ frag.start,
401
+ PlaylistLevelType.MAIN,
402
+ );
403
+ if (mainFrag === null) {
404
+ return;
405
+ }
406
+ // Bridge gaps in main buffer (also prevents loop loading at gaps)
407
+ atGap ||= !!mainFrag.gap || mainBufferInfo.len === 0;
408
+ if (
409
+ !atGap ||
410
+ (bufferInfo.nextStart && bufferInfo.nextStart < mainFrag.end)
411
+ ) {
412
+ return;
413
+ }
415
414
  }
416
415
  }
417
416
 
418
417
  this.loadFragment(frag, levelInfo, targetBufferTime);
419
418
  }
420
419
 
421
- protected getMaxBufferLength(mainBufferLength?: number): number {
422
- const maxConfigBuffer = super.getMaxBufferLength();
423
- if (!mainBufferLength) {
424
- return maxConfigBuffer;
425
- }
426
- return Math.min(
427
- Math.max(maxConfigBuffer, mainBufferLength),
428
- this.config.maxMaxBufferLength,
429
- );
430
- }
431
-
432
420
  onMediaDetaching() {
433
421
  this.videoBuffer = null;
434
422
  this.bufferFlushed = this.flushing = false;
@@ -1102,7 +1102,10 @@ export default class BaseStreamController
1102
1102
  // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos
1103
1103
  if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
1104
1104
  const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
1105
- if (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) {
1105
+ if (
1106
+ bufferedFragAtPos &&
1107
+ (bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)
1108
+ ) {
1106
1109
  return BufferHelper.bufferInfo(
1107
1110
  bufferable,
1108
1111
  pos,
@@ -1115,7 +1118,7 @@ export default class BaseStreamController
1115
1118
 
1116
1119
  protected getMaxBufferLength(levelBitrate?: number): number {
1117
1120
  const { config } = this;
1118
- let maxBufLen;
1121
+ let maxBufLen: number;
1119
1122
  if (levelBitrate) {
1120
1123
  maxBufLen = Math.max(
1121
1124
  (8 * config.maxBufferSize) / levelBitrate,
@@ -7,7 +7,12 @@ import {
7
7
  pickMostCompleteCodecName,
8
8
  } from '../utils/codecs';
9
9
  import { getMediaSource } from '../utils/mediasource-helper';
10
- import { ElementaryStreamTypes } from '../loader/fragment';
10
+ import {
11
+ ElementaryStreamTypes,
12
+ type Part,
13
+ type Fragment,
14
+ } from '../loader/fragment';
15
+ import { PlaylistLevelType } from '../types/loader';
11
16
  import type { TrackSet } from '../types/track';
12
17
  import BufferOperationQueue from './buffer-operation-queue';
13
18
  import {
@@ -31,6 +36,7 @@ import type {
31
36
  import type { ComponentAPI } from '../types/component-api';
32
37
  import type { ChunkMetadata } from '../types/transmuxer';
33
38
  import type Hls from '../hls';
39
+ import type { FragmentTracker } from './fragment-tracker';
34
40
  import type { LevelDetails } from '../loader/level-details';
35
41
  import type { HlsConfig } from '../config';
36
42
 
@@ -53,6 +59,7 @@ export default class BufferController extends Logger implements ComponentAPI {
53
59
  private listeners!: SourceBufferListeners;
54
60
 
55
61
  private hls: Hls;
62
+ private fragmentTracker: FragmentTracker;
56
63
 
57
64
  // The number of BUFFER_CODEC events received before any sourceBuffers are created
58
65
  public bufferCodecEventsExpected: number = 0;
@@ -69,6 +76,14 @@ export default class BufferController extends Logger implements ComponentAPI {
69
76
  // Last MP3 audio chunk appended
70
77
  private lastMpegAudioChunk: ChunkMetadata | null = null;
71
78
 
79
+ // Audio fragment blocked from appending until corresponding video appends or context changes
80
+ private blockedAudioAppend: {
81
+ op: BufferOperation;
82
+ frag: Fragment | Part;
83
+ } | null = null;
84
+ // Keep track of video append position for unblocking audio
85
+ private lastVideoAppendEnd: number = 0;
86
+
72
87
  private appendSource: boolean;
73
88
 
74
89
  // counters
@@ -82,9 +97,10 @@ export default class BufferController extends Logger implements ComponentAPI {
82
97
  public pendingTracks: TrackSet = {};
83
98
  public sourceBuffer!: SourceBuffers;
84
99
 
85
- constructor(hls: Hls) {
100
+ constructor(hls: Hls, fragmentTracker: FragmentTracker) {
86
101
  super('buffer-controller', hls.logger);
87
102
  this.hls = hls;
103
+ this.fragmentTracker = fragmentTracker;
88
104
  this.appendSource = hls.config.preferManagedMediaSource;
89
105
  this._initSourceBuffer();
90
106
  this.registerListeners();
@@ -102,7 +118,7 @@ export default class BufferController extends Logger implements ComponentAPI {
102
118
  this.details = null;
103
119
  this.lastMpegAudioChunk = null;
104
120
  // @ts-ignore
105
- this.hls = null;
121
+ this.hls = this.fragmentTracker = null;
106
122
  // @ts-ignore
107
123
  this._onMediaSourceOpen = this._onMediaSourceClose = null;
108
124
  // @ts-ignore
@@ -157,6 +173,8 @@ export default class BufferController extends Logger implements ComponentAPI {
157
173
  audiovideo: 0,
158
174
  };
159
175
  this.lastMpegAudioChunk = null;
176
+ this.blockedAudioAppend = null;
177
+ this.lastVideoAppendEnd = 0;
160
178
  }
161
179
 
162
180
  private onManifestLoading() {
@@ -321,11 +339,11 @@ export default class BufferController extends Logger implements ComponentAPI {
321
339
  ) {
322
340
  const sourceBufferCount = this.getSourceBufferTypes().length;
323
341
  const trackNames = Object.keys(data);
324
- trackNames.forEach((trackName) => {
342
+ trackNames.forEach((trackName: SourceBufferName) => {
325
343
  if (sourceBufferCount) {
326
344
  // check if SourceBuffer codec needs to change
327
345
  const track = this.tracks[trackName];
328
- if (track && typeof track.buffer.changeType === 'function') {
346
+ if (track && typeof track.buffer?.changeType === 'function') {
329
347
  const { id, codec, levelCodec, container, metadata } =
330
348
  data[trackName];
331
349
  const currentCodecFull = pickMostCompleteCodecName(
@@ -389,7 +407,7 @@ export default class BufferController extends Logger implements ComponentAPI {
389
407
  }
390
408
  }
391
409
 
392
- protected appendChangeType(type, mimeType) {
410
+ protected appendChangeType(type: SourceBufferName, mimeType: string) {
393
411
  const { operationQueue } = this;
394
412
  const operation: BufferOperation = {
395
413
  execute: () => {
@@ -410,14 +428,52 @@ export default class BufferController extends Logger implements ComponentAPI {
410
428
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
411
429
  }
412
430
 
431
+ private blockAudio(partOrFrag: Fragment | Part) {
432
+ const pStart = partOrFrag.start;
433
+ const pTime = pStart + partOrFrag.duration * 0.05;
434
+ const atGap =
435
+ this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)
436
+ ?.gap === true;
437
+ if (atGap) {
438
+ return;
439
+ }
440
+ const op: BufferOperation = {
441
+ execute: () => {
442
+ if (
443
+ this.lastVideoAppendEnd > pTime ||
444
+ (this.sourceBuffer.video &&
445
+ BufferHelper.isBuffered(this.sourceBuffer.video, pTime)) ||
446
+ this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)
447
+ ?.gap === true
448
+ ) {
449
+ this.blockedAudioAppend = null;
450
+ this.operationQueue.shiftAndExecuteNext('audio');
451
+ }
452
+ },
453
+ onStart: () => {},
454
+ onComplete: () => {},
455
+ onError: () => {},
456
+ };
457
+ this.blockedAudioAppend = { op, frag: partOrFrag };
458
+ this.operationQueue.append(op, 'audio', true);
459
+ }
460
+
461
+ private unblockAudio() {
462
+ const blockedAudioAppend = this.blockedAudioAppend;
463
+ if (blockedAudioAppend) {
464
+ this.blockedAudioAppend = null;
465
+ this.operationQueue.unblockAudio(blockedAudioAppend.op);
466
+ }
467
+ }
468
+
413
469
  protected onBufferAppending(
414
470
  event: Events.BUFFER_APPENDING,
415
471
  eventData: BufferAppendingData,
416
472
  ) {
417
- const { hls, operationQueue, tracks } = this;
418
- const { data, type, frag, part, chunkMeta } = eventData;
473
+ const { operationQueue, tracks } = this;
474
+ const { data, type, parent, frag, part, chunkMeta } = eventData;
419
475
  const chunkStats = chunkMeta.buffering[type];
420
-
476
+ const sn = frag.sn;
421
477
  const bufferAppendingStart = self.performance.now();
422
478
  chunkStats.start = bufferAppendingStart;
423
479
  const fragBuffering = frag.stats.buffering;
@@ -444,7 +500,44 @@ export default class BufferController extends Logger implements ComponentAPI {
444
500
  this.lastMpegAudioChunk = chunkMeta;
445
501
  }
446
502
 
447
- const fragStart = frag.start;
503
+ // Block audio append until overlapping video append
504
+ const videoSb = this.sourceBuffer.video;
505
+ if (videoSb && sn !== 'initSegment') {
506
+ const partOrFrag = part || frag;
507
+ const blockedAudioAppend = this.blockedAudioAppend;
508
+ if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) {
509
+ const pStart = partOrFrag.start;
510
+ const pTime = pStart + partOrFrag.duration * 0.05;
511
+ const vbuffered = videoSb.buffered;
512
+ const vappending = this.operationQueue.current('video');
513
+ if (!vbuffered.length && !vappending) {
514
+ // wait for video before appending audio
515
+ this.blockAudio(partOrFrag);
516
+ } else if (
517
+ !vappending &&
518
+ !BufferHelper.isBuffered(videoSb, pTime) &&
519
+ this.lastVideoAppendEnd < pTime
520
+ ) {
521
+ // audio is ahead of video
522
+ this.blockAudio(partOrFrag);
523
+ }
524
+ } else if (type === 'video') {
525
+ const videoAppendEnd = partOrFrag.end;
526
+ if (blockedAudioAppend) {
527
+ const audioStart = blockedAudioAppend.frag.start;
528
+ if (
529
+ videoAppendEnd > audioStart ||
530
+ videoAppendEnd < this.lastVideoAppendEnd ||
531
+ BufferHelper.isBuffered(videoSb, audioStart)
532
+ ) {
533
+ this.unblockAudio();
534
+ }
535
+ }
536
+ this.lastVideoAppendEnd = videoAppendEnd;
537
+ }
538
+ }
539
+
540
+ const fragStart = (part || frag).start;
448
541
  const operation: BufferOperation = {
449
542
  execute: () => {
450
543
  chunkStats.executeStart = self.performance.now();
@@ -454,7 +547,7 @@ export default class BufferController extends Logger implements ComponentAPI {
454
547
  const delta = fragStart - sb.timestampOffset;
455
548
  if (Math.abs(delta) >= 0.1) {
456
549
  this.log(
457
- `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`,
550
+ `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${sn})`,
458
551
  );
459
552
  sb.timestampOffset = fragStart;
460
553
  }
@@ -523,30 +616,27 @@ export default class BufferController extends Logger implements ComponentAPI {
523
616
  browser is able to evict some data from sourcebuffer. Retrying can help recover.
524
617
  */
525
618
  this.warn(
526
- `Failed ${appendErrorCount}/${hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
619
+ `Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
527
620
  );
528
- if (appendErrorCount >= hls.config.appendErrorMaxRetry) {
621
+ if (appendErrorCount >= this.hls.config.appendErrorMaxRetry) {
529
622
  event.fatal = true;
530
623
  }
531
624
  }
532
- hls.trigger(Events.ERROR, event);
625
+ this.hls.trigger(Events.ERROR, event);
533
626
  },
534
627
  };
535
628
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
536
629
  }
537
630
 
538
- protected onBufferFlushing(
539
- event: Events.BUFFER_FLUSHING,
540
- data: BufferFlushingData,
541
- ) {
542
- const { operationQueue } = this;
543
- const flushOperation = (type: SourceBufferName): BufferOperation => ({
544
- execute: this.removeExecutor.bind(
545
- this,
546
- type,
547
- data.startOffset,
548
- data.endOffset,
549
- ),
631
+ private getFlushOp(
632
+ type: SourceBufferName,
633
+ start: number,
634
+ end: number,
635
+ ): BufferOperation {
636
+ return {
637
+ execute: () => {
638
+ this.removeExecutor(type, start, end);
639
+ },
550
640
  onStart: () => {
551
641
  // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
552
642
  },
@@ -557,13 +647,26 @@ export default class BufferController extends Logger implements ComponentAPI {
557
647
  onError: (error: Error) => {
558
648
  this.warn(`Failed to remove from ${type} SourceBuffer`, error);
559
649
  },
560
- });
650
+ };
651
+ }
561
652
 
562
- if (data.type) {
563
- operationQueue.append(flushOperation(data.type), data.type);
653
+ protected onBufferFlushing(
654
+ event: Events.BUFFER_FLUSHING,
655
+ data: BufferFlushingData,
656
+ ) {
657
+ const { operationQueue } = this;
658
+ const { type, startOffset, endOffset } = data;
659
+ if (type) {
660
+ operationQueue.append(
661
+ this.getFlushOp(type, startOffset, endOffset),
662
+ type,
663
+ );
564
664
  } else {
565
- this.getSourceBufferTypes().forEach((type: SourceBufferName) => {
566
- operationQueue.append(flushOperation(type), type);
665
+ this.getSourceBufferTypes().forEach((sbType: SourceBufferName) => {
666
+ operationQueue.append(
667
+ this.getFlushOp(sbType, startOffset, endOffset),
668
+ sbType,
669
+ );
567
670
  });
568
671
  }
569
672
  }
@@ -616,6 +719,9 @@ export default class BufferController extends Logger implements ComponentAPI {
616
719
  // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
617
720
  // an undefined data.type will mark all buffers as EOS.
618
721
  protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
722
+ if (data.type === 'video') {
723
+ this.unblockAudio();
724
+ }
619
725
  const ended = this.getSourceBufferTypes().reduce((acc, type) => {
620
726
  const sb = this.sourceBuffer[type];
621
727
  if (sb && (!data.type || data.type === type)) {
@@ -661,11 +767,14 @@ export default class BufferController extends Logger implements ComponentAPI {
661
767
  return;
662
768
  }
663
769
  this.details = details;
664
-
770
+ const durationAndRange = this.getDurationAndRange();
771
+ if (!durationAndRange) {
772
+ return;
773
+ }
665
774
  if (this.getSourceBufferTypes().length) {
666
- this.blockBuffers(this.updateMediaElementDuration.bind(this));
775
+ this.blockBuffers(() => this.updateMediaSource(durationAndRange));
667
776
  } else {
668
- this.updateMediaElementDuration();
777
+ this.updateMediaSource(durationAndRange);
669
778
  }
670
779
  }
671
780
 
@@ -817,14 +926,18 @@ export default class BufferController extends Logger implements ComponentAPI {
817
926
  * 'liveDurationInfinity` is set to `true`
818
927
  * More details: https://github.com/video-dev/hls.js/issues/355
819
928
  */
820
- private updateMediaElementDuration() {
929
+ private getDurationAndRange(): {
930
+ duration: number;
931
+ start?: number;
932
+ end?: number;
933
+ } | null {
821
934
  if (
822
935
  !this.details ||
823
936
  !this.media ||
824
937
  !this.mediaSource ||
825
938
  this.mediaSource.readyState !== 'open'
826
939
  ) {
827
- return;
940
+ return null;
828
941
  }
829
942
  const { details, hls, media, mediaSource } = this;
830
943
  const levelDuration = details.fragments[0].start + details.totalduration;
@@ -836,31 +949,47 @@ export default class BufferController extends Logger implements ComponentAPI {
836
949
  if (details.live && hls.config.liveDurationInfinity) {
837
950
  // Override duration to Infinity
838
951
  mediaSource.duration = Infinity;
839
- this.updateSeekableRange(details);
952
+ const len = details.fragments.length;
953
+ if (len && details.live && !!mediaSource.setLiveSeekableRange) {
954
+ const start = Math.max(0, details.fragments[0].start);
955
+ const end = Math.max(start, start + details.totalduration);
956
+ return { duration: Infinity, start, end };
957
+ }
958
+ return { duration: Infinity };
840
959
  } else if (
841
960
  (levelDuration > msDuration && levelDuration > mediaDuration) ||
842
961
  !Number.isFinite(mediaDuration)
843
962
  ) {
844
- // levelDuration was the last value we set.
845
- // not using mediaSource.duration as the browser may tweak this value
846
- // only update Media Source duration if its value increase, this is to avoid
847
- // flushing already buffered portion when switching between quality level
848
- this.log(`Updating Media Source duration to ${levelDuration.toFixed(3)}`);
849
- mediaSource.duration = levelDuration;
963
+ return { duration: levelDuration };
850
964
  }
965
+ return null;
851
966
  }
852
967
 
853
- updateSeekableRange(levelDetails) {
854
- const mediaSource = this.mediaSource;
855
- const fragments = levelDetails.fragments;
856
- const len = fragments.length;
857
- if (len && levelDetails.live && mediaSource?.setLiveSeekableRange) {
858
- const start = Math.max(0, fragments[0].start);
859
- const end = Math.max(start, start + levelDetails.totalduration);
968
+ private updateMediaSource({
969
+ duration,
970
+ start,
971
+ end,
972
+ }: {
973
+ duration: number;
974
+ start?: number;
975
+ end?: number;
976
+ }) {
977
+ if (
978
+ !this.media ||
979
+ !this.mediaSource ||
980
+ this.mediaSource.readyState !== 'open'
981
+ ) {
982
+ return;
983
+ }
984
+ if (Number.isFinite(duration)) {
985
+ this.log(`Updating Media Source duration to ${duration.toFixed(3)}`);
986
+ }
987
+ this.mediaSource.duration = duration;
988
+ if (start !== undefined && end !== undefined) {
860
989
  this.log(
861
- `Media Source duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
990
+ `Media Source duration is set to ${this.mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
862
991
  );
863
- mediaSource.setLiveSeekableRange(start, end);
992
+ this.mediaSource.setLiveSeekableRange(start, end);
864
993
  }
865
994
  }
866
995
 
@@ -979,7 +1108,10 @@ export default class BufferController extends Logger implements ComponentAPI {
979
1108
  this.log('Media source opened');
980
1109
  if (media) {
981
1110
  media.removeEventListener('emptied', this._onMediaEmptied);
982
- this.updateMediaElementDuration();
1111
+ const durationAndRange = this.getDurationAndRange();
1112
+ if (durationAndRange) {
1113
+ this.updateMediaSource(durationAndRange);
1114
+ }
983
1115
  this.hls.trigger(Events.MEDIA_ATTACHED, {
984
1116
  media,
985
1117
  mediaSource: mediaSource as MediaSource,
@@ -1100,7 +1232,7 @@ export default class BufferController extends Logger implements ComponentAPI {
1100
1232
  }
1101
1233
  return;
1102
1234
  }
1103
-
1235
+ sb.ending = false;
1104
1236
  sb.ended = false;
1105
1237
  sb.appendBuffer(data);
1106
1238
  }
@@ -1123,10 +1255,14 @@ export default class BufferController extends Logger implements ComponentAPI {
1123
1255
  const blockingOperations = buffers.map((type) =>
1124
1256
  operationQueue.appendBlocker(type as SourceBufferName),
1125
1257
  );
1126
- Promise.all(blockingOperations).then(() => {
1258
+ const audioBlocked = buffers.length > 1 && !!this.blockedAudioAppend;
1259
+ if (audioBlocked) {
1260
+ this.unblockAudio();
1261
+ }
1262
+ Promise.all(blockingOperations).then((result) => {
1127
1263
  // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
1128
1264
  onUnblocked();
1129
- buffers.forEach((type) => {
1265
+ buffers.forEach((type, i) => {
1130
1266
  const sb = this.sourceBuffer[type];
1131
1267
  // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
1132
1268
  // true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration)
@@ -30,26 +30,23 @@ export default class BufferOperationQueue {
30
30
  }
31
31
  }
32
32
 
33
- public insertAbort(operation: BufferOperation, type: SourceBufferName) {
34
- const queue = this.queues[type];
35
- queue.unshift(operation);
36
- this.executeNext(type);
37
- }
38
-
39
- public appendBlocker(type: SourceBufferName): Promise<{}> {
40
- let execute;
41
- const promise: Promise<{}> = new Promise((resolve) => {
42
- execute = resolve;
33
+ public appendBlocker(type: SourceBufferName): Promise<void> {
34
+ return new Promise((resolve) => {
35
+ const operation: BufferOperation = {
36
+ execute: resolve,
37
+ onStart: () => {},
38
+ onComplete: () => {},
39
+ onError: () => {},
40
+ };
41
+ this.append(operation, type);
43
42
  });
44
- const operation: BufferOperation = {
45
- execute,
46
- onStart: () => {},
47
- onComplete: () => {},
48
- onError: () => {},
49
- };
43
+ }
50
44
 
51
- this.append(operation, type);
52
- return promise;
45
+ unblockAudio(op: BufferOperation) {
46
+ const queue = this.queues.audio;
47
+ if (queue[0] === op) {
48
+ this.shiftAndExecuteNext('audio');
49
+ }
53
50
  }
54
51
 
55
52
  public executeNext(type: SourceBufferName) {
@@ -80,7 +77,7 @@ export default class BufferOperationQueue {
80
77
  this.executeNext(type);
81
78
  }
82
79
 
83
- public current(type: SourceBufferName) {
80
+ public current(type: SourceBufferName): BufferOperation {
84
81
  return this.queues[type][0];
85
82
  }
86
83
  }
@@ -107,12 +107,23 @@ export class FragmentTracker implements ComponentAPI {
107
107
  public getBufferedFrag(
108
108
  position: number,
109
109
  levelType: PlaylistLevelType,
110
+ ): Fragment | null {
111
+ return this.getFragAtPos(position, levelType, true);
112
+ }
113
+
114
+ public getFragAtPos(
115
+ position: number,
116
+ levelType: PlaylistLevelType,
117
+ buffered?: boolean,
110
118
  ): Fragment | null {
111
119
  const { fragments } = this;
112
120
  const keys = Object.keys(fragments);
113
121
  for (let i = keys.length; i--; ) {
114
122
  const fragmentEntity = fragments[keys[i]];
115
- if (fragmentEntity?.body.type === levelType && fragmentEntity.buffered) {
123
+ if (
124
+ fragmentEntity?.body.type === levelType &&
125
+ (!buffered || fragmentEntity.buffered)
126
+ ) {
116
127
  const frag = fragmentEntity.body;
117
128
  if (frag.start <= position && position <= frag.end) {
118
129
  return frag;
@@ -401,7 +412,7 @@ export class FragmentTracker implements ComponentAPI {
401
412
  event: Events.BUFFER_APPENDED,
402
413
  data: BufferAppendedData,
403
414
  ) {
404
- const { frag, part, timeRanges } = data;
415
+ const { frag, part, timeRanges, type } = data;
405
416
  if (frag.sn === 'initSegment') {
406
417
  return;
407
418
  }
@@ -415,15 +426,8 @@ export class FragmentTracker implements ComponentAPI {
415
426
  }
416
427
  // Store the latest timeRanges loaded in the buffer
417
428
  this.timeRanges = timeRanges;
418
- Object.keys(timeRanges).forEach((elementaryStream: SourceBufferName) => {
419
- const timeRange = timeRanges[elementaryStream] as TimeRanges;
420
- this.detectEvictedFragments(
421
- elementaryStream,
422
- timeRange,
423
- playlistType,
424
- part,
425
- );
426
- });
429
+ const timeRange = timeRanges[type] as TimeRanges;
430
+ this.detectEvictedFragments(type, timeRange, playlistType, part);
427
431
  }
428
432
 
429
433
  private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
@@ -1338,6 +1338,15 @@ export default class StreamController
1338
1338
  );
1339
1339
  }
1340
1340
 
1341
+ public get maxBufferLength(): number {
1342
+ const { levels, level } = this;
1343
+ const levelInfo = levels?.[level];
1344
+ if (!levelInfo) {
1345
+ return this.config.maxBufferLength;
1346
+ }
1347
+ return this.getMaxBufferLength(levelInfo.maxBitrate);
1348
+ }
1349
+
1341
1350
  private backtrack(frag: Fragment) {
1342
1351
  this.couldBacktrack = true;
1343
1352
  // Causes findFragments to backtrack through fragments to find the keyframe