hls.js 1.5.13 → 1.5.14-0.canary.10415

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,5 +1,5 @@
1
1
  import { Events } from '../events';
2
- import { logger } from '../utils/logger';
2
+ import { Logger } from '../utils/logger';
3
3
  import { ErrorDetails, ErrorTypes } from '../errors';
4
4
  import { BufferHelper } from '../utils/buffer-helper';
5
5
  import {
@@ -10,7 +10,12 @@ import {
10
10
  getMediaSource,
11
11
  isManagedMediaSource,
12
12
  } from '../utils/mediasource-helper';
13
- import { ElementaryStreamTypes } from '../loader/fragment';
13
+ import {
14
+ ElementaryStreamTypes,
15
+ type Part,
16
+ type Fragment,
17
+ } from '../loader/fragment';
18
+ import { PlaylistLevelType } from '../types/loader';
14
19
  import type { TrackSet } from '../types/track';
15
20
  import BufferOperationQueue from './buffer-operation-queue';
16
21
  import {
@@ -34,6 +39,7 @@ import type {
34
39
  import type { ComponentAPI } from '../types/component-api';
35
40
  import type { ChunkMetadata } from '../types/transmuxer';
36
41
  import type Hls from '../hls';
42
+ import type { FragmentTracker } from './fragment-tracker';
37
43
  import type { LevelDetails } from '../loader/level-details';
38
44
  import type { HlsConfig } from '../config';
39
45
 
@@ -45,7 +51,7 @@ interface BufferedChangeEvent extends Event {
45
51
  readonly removedRanges?: TimeRanges;
46
52
  }
47
53
 
48
- export default class BufferController implements ComponentAPI {
54
+ export default class BufferController extends Logger implements ComponentAPI {
49
55
  // The level details used to determine duration, target-duration and live
50
56
  private details: LevelDetails | null = null;
51
57
  // cache the self generated object url to detect hijack of video tag
@@ -56,6 +62,7 @@ export default class BufferController implements ComponentAPI {
56
62
  private listeners!: SourceBufferListeners;
57
63
 
58
64
  private hls: Hls;
65
+ private fragmentTracker: FragmentTracker;
59
66
 
60
67
  // The number of BUFFER_CODEC events received before any sourceBuffers are created
61
68
  public bufferCodecEventsExpected: number = 0;
@@ -72,6 +79,14 @@ export default class BufferController implements ComponentAPI {
72
79
  // Last MP3 audio chunk appended
73
80
  private lastMpegAudioChunk: ChunkMetadata | null = null;
74
81
 
82
+ // Audio fragment blocked from appending until corresponding video appends or context changes
83
+ private blockedAudioAppend: {
84
+ op: BufferOperation;
85
+ frag: Fragment | Part;
86
+ } | null = null;
87
+ // Keep track of video append position for unblocking audio
88
+ private lastVideoAppendEnd: number = 0;
89
+
75
90
  private appendSource: boolean;
76
91
 
77
92
  // counters
@@ -85,19 +100,14 @@ export default class BufferController implements ComponentAPI {
85
100
  public pendingTracks: TrackSet = {};
86
101
  public sourceBuffer!: SourceBuffers;
87
102
 
88
- protected log: (msg: any) => void;
89
- protected warn: (msg: any, obj?: any) => void;
90
- protected error: (msg: any, obj?: any) => void;
91
-
92
- constructor(hls: Hls) {
103
+ constructor(hls: Hls, fragmentTracker: FragmentTracker) {
104
+ super('buffer-controller', hls.logger);
93
105
  this.hls = hls;
94
- const logPrefix = '[buffer-controller]';
106
+ this.fragmentTracker = fragmentTracker;
95
107
  this.appendSource = isManagedMediaSource(
96
108
  getMediaSource(hls.config.preferManagedMediaSource),
97
109
  );
98
- this.log = logger.log.bind(logger, logPrefix);
99
- this.warn = logger.warn.bind(logger, logPrefix);
100
- this.error = logger.error.bind(logger, logPrefix);
110
+
101
111
  this._initSourceBuffer();
102
112
  this.registerListeners();
103
113
  }
@@ -114,7 +124,13 @@ export default class BufferController implements ComponentAPI {
114
124
  this.details = null;
115
125
  this.lastMpegAudioChunk = null;
116
126
  // @ts-ignore
117
- this.hls = null;
127
+ this.hls = this.fragmentTracker = null;
128
+ // @ts-ignore
129
+ this._onMediaSourceOpen = this._onMediaSourceClose = null;
130
+ // @ts-ignore
131
+ this._onMediaSourceEnded = null;
132
+ // @ts-ignore
133
+ this._onStartStreaming = this._onEndStreaming = null;
118
134
  }
119
135
 
120
136
  protected registerListeners() {
@@ -131,6 +147,7 @@ export default class BufferController implements ComponentAPI {
131
147
  hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
132
148
  hls.on(Events.FRAG_PARSED, this.onFragParsed, this);
133
149
  hls.on(Events.FRAG_CHANGED, this.onFragChanged, this);
150
+ hls.on(Events.ERROR, this.onError, this);
134
151
  }
135
152
 
136
153
  protected unregisterListeners() {
@@ -147,6 +164,7 @@ export default class BufferController implements ComponentAPI {
147
164
  hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
148
165
  hls.off(Events.FRAG_PARSED, this.onFragParsed, this);
149
166
  hls.off(Events.FRAG_CHANGED, this.onFragChanged, this);
167
+ hls.off(Events.ERROR, this.onError, this);
150
168
  }
151
169
 
152
170
  private _initSourceBuffer() {
@@ -157,12 +175,10 @@ export default class BufferController implements ComponentAPI {
157
175
  video: [],
158
176
  audiovideo: [],
159
177
  };
160
- this.appendErrors = {
161
- audio: 0,
162
- video: 0,
163
- audiovideo: 0,
164
- };
178
+ this.resetAppendErrors();
165
179
  this.lastMpegAudioChunk = null;
180
+ this.blockedAudioAppend = null;
181
+ this.lastVideoAppendEnd = 0;
166
182
  }
167
183
 
168
184
  private onManifestLoading() {
@@ -270,37 +286,38 @@ export default class BufferController implements ComponentAPI {
270
286
  mediaSource.removeEventListener('endstreaming', this._onEndStreaming);
271
287
  }
272
288
 
273
- // Detach properly the MediaSource from the HTMLMediaElement as
274
- // suggested in https://github.com/w3c/media-source/issues/53.
275
- if (media) {
276
- media.removeEventListener('emptied', this._onMediaEmptied);
277
- if (_objectUrl) {
278
- self.URL.revokeObjectURL(_objectUrl);
279
- }
289
+ this.mediaSource = null;
290
+ this._objectUrl = null;
291
+ }
280
292
 
281
- // clean up video tag src only if it's our own url. some external libraries might
282
- // hijack the video tag and change its 'src' without destroying the Hls instance first
283
- if (this.mediaSrc === _objectUrl) {
284
- media.removeAttribute('src');
285
- if (this.appendSource) {
286
- removeSourceChildren(media);
287
- }
288
- media.load();
289
- } else {
290
- this.warn(
291
- 'media|source.src was changed by a third party - skip cleanup',
292
- );
293
- }
293
+ // Detach properly the MediaSource from the HTMLMediaElement as
294
+ // suggested in https://github.com/w3c/media-source/issues/53.
295
+ if (media) {
296
+ media.removeEventListener('emptied', this._onMediaEmptied);
297
+ if (_objectUrl) {
298
+ self.URL.revokeObjectURL(_objectUrl);
294
299
  }
295
300
 
296
- this.mediaSource = null;
301
+ // clean up video tag src only if it's our own url. some external libraries might
302
+ // hijack the video tag and change its 'src' without destroying the Hls instance first
303
+ if (this.mediaSrc === _objectUrl) {
304
+ media.removeAttribute('src');
305
+ if (this.appendSource) {
306
+ removeSourceChildren(media);
307
+ }
308
+ media.load();
309
+ } else {
310
+ this.warn(
311
+ 'media|source.src was changed by a third party - skip cleanup',
312
+ );
313
+ }
297
314
  this.media = null;
298
- this._objectUrl = null;
299
- this.bufferCodecEventsExpected = this._bufferCodecEventsTotal;
300
- this.pendingTracks = {};
301
- this.tracks = {};
302
315
  }
303
316
 
317
+ this.bufferCodecEventsExpected = this._bufferCodecEventsTotal;
318
+ this.pendingTracks = {};
319
+ this.tracks = {};
320
+
304
321
  this.hls.trigger(Events.MEDIA_DETACHED, undefined);
305
322
  }
306
323
 
@@ -309,6 +326,7 @@ export default class BufferController implements ComponentAPI {
309
326
  this.resetBuffer(type);
310
327
  });
311
328
  this._initSourceBuffer();
329
+ this.hls.resumeBuffering();
312
330
  }
313
331
 
314
332
  private resetBuffer(type: SourceBufferName) {
@@ -334,11 +352,11 @@ export default class BufferController implements ComponentAPI {
334
352
  ) {
335
353
  const sourceBufferCount = this.getSourceBufferTypes().length;
336
354
  const trackNames = Object.keys(data);
337
- trackNames.forEach((trackName) => {
355
+ trackNames.forEach((trackName: SourceBufferName) => {
338
356
  if (sourceBufferCount) {
339
357
  // check if SourceBuffer codec needs to change
340
358
  const track = this.tracks[trackName];
341
- if (track && typeof track.buffer.changeType === 'function') {
359
+ if (track && typeof track.buffer?.changeType === 'function') {
342
360
  const { id, codec, levelCodec, container, metadata } =
343
361
  data[trackName];
344
362
  const currentCodecFull = pickMostCompleteCodecName(
@@ -402,7 +420,7 @@ export default class BufferController implements ComponentAPI {
402
420
  }
403
421
  }
404
422
 
405
- protected appendChangeType(type, mimeType) {
423
+ protected appendChangeType(type: SourceBufferName, mimeType: string) {
406
424
  const { operationQueue } = this;
407
425
  const operation: BufferOperation = {
408
426
  execute: () => {
@@ -423,14 +441,52 @@ export default class BufferController implements ComponentAPI {
423
441
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
424
442
  }
425
443
 
444
+ private blockAudio(partOrFrag: Fragment | Part) {
445
+ const pStart = partOrFrag.start;
446
+ const pTime = pStart + partOrFrag.duration * 0.05;
447
+ const atGap =
448
+ this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)
449
+ ?.gap === true;
450
+ if (atGap) {
451
+ return;
452
+ }
453
+ const op: BufferOperation = {
454
+ execute: () => {
455
+ if (
456
+ this.lastVideoAppendEnd > pTime ||
457
+ (this.sourceBuffer.video &&
458
+ BufferHelper.isBuffered(this.sourceBuffer.video, pTime)) ||
459
+ this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)
460
+ ?.gap === true
461
+ ) {
462
+ this.blockedAudioAppend = null;
463
+ this.operationQueue.shiftAndExecuteNext('audio');
464
+ }
465
+ },
466
+ onStart: () => {},
467
+ onComplete: () => {},
468
+ onError: () => {},
469
+ };
470
+ this.blockedAudioAppend = { op, frag: partOrFrag };
471
+ this.operationQueue.append(op, 'audio', true);
472
+ }
473
+
474
+ private unblockAudio() {
475
+ const blockedAudioAppend = this.blockedAudioAppend;
476
+ if (blockedAudioAppend) {
477
+ this.blockedAudioAppend = null;
478
+ this.operationQueue.unblockAudio(blockedAudioAppend.op);
479
+ }
480
+ }
481
+
426
482
  protected onBufferAppending(
427
483
  event: Events.BUFFER_APPENDING,
428
484
  eventData: BufferAppendingData,
429
485
  ) {
430
- const { hls, operationQueue, tracks } = this;
431
- const { data, type, frag, part, chunkMeta } = eventData;
486
+ const { operationQueue, tracks } = this;
487
+ const { data, type, parent, frag, part, chunkMeta } = eventData;
432
488
  const chunkStats = chunkMeta.buffering[type];
433
-
489
+ const sn = frag.sn;
434
490
  const bufferAppendingStart = self.performance.now();
435
491
  chunkStats.start = bufferAppendingStart;
436
492
  const fragBuffering = frag.stats.buffering;
@@ -457,7 +513,44 @@ export default class BufferController implements ComponentAPI {
457
513
  this.lastMpegAudioChunk = chunkMeta;
458
514
  }
459
515
 
460
- const fragStart = frag.start;
516
+ // Block audio append until overlapping video append
517
+ const videoSb = this.sourceBuffer.video;
518
+ if (videoSb && sn !== 'initSegment') {
519
+ const partOrFrag = part || frag;
520
+ const blockedAudioAppend = this.blockedAudioAppend;
521
+ if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) {
522
+ const pStart = partOrFrag.start;
523
+ const pTime = pStart + partOrFrag.duration * 0.05;
524
+ const vbuffered = videoSb.buffered;
525
+ const vappending = this.operationQueue.current('video');
526
+ if (!vbuffered.length && !vappending) {
527
+ // wait for video before appending audio
528
+ this.blockAudio(partOrFrag);
529
+ } else if (
530
+ !vappending &&
531
+ !BufferHelper.isBuffered(videoSb, pTime) &&
532
+ this.lastVideoAppendEnd < pTime
533
+ ) {
534
+ // audio is ahead of video
535
+ this.blockAudio(partOrFrag);
536
+ }
537
+ } else if (type === 'video') {
538
+ const videoAppendEnd = partOrFrag.end;
539
+ if (blockedAudioAppend) {
540
+ const audioStart = blockedAudioAppend.frag.start;
541
+ if (
542
+ videoAppendEnd > audioStart ||
543
+ videoAppendEnd < this.lastVideoAppendEnd ||
544
+ BufferHelper.isBuffered(videoSb, audioStart)
545
+ ) {
546
+ this.unblockAudio();
547
+ }
548
+ }
549
+ this.lastVideoAppendEnd = videoAppendEnd;
550
+ }
551
+ }
552
+
553
+ const fragStart = (part || frag).start;
461
554
  const operation: BufferOperation = {
462
555
  execute: () => {
463
556
  chunkStats.executeStart = self.performance.now();
@@ -467,7 +560,7 @@ export default class BufferController implements ComponentAPI {
467
560
  const delta = fragStart - sb.timestampOffset;
468
561
  if (Math.abs(delta) >= 0.1) {
469
562
  this.log(
470
- `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`,
563
+ `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${sn})`,
471
564
  );
472
565
  sb.timestampOffset = fragStart;
473
566
  }
@@ -536,30 +629,27 @@ export default class BufferController implements ComponentAPI {
536
629
  browser is able to evict some data from sourcebuffer. Retrying can help recover.
537
630
  */
538
631
  this.warn(
539
- `Failed ${appendErrorCount}/${hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
632
+ `Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
540
633
  );
541
- if (appendErrorCount >= hls.config.appendErrorMaxRetry) {
634
+ if (appendErrorCount >= this.hls.config.appendErrorMaxRetry) {
542
635
  event.fatal = true;
543
636
  }
544
637
  }
545
- hls.trigger(Events.ERROR, event);
638
+ this.hls.trigger(Events.ERROR, event);
546
639
  },
547
640
  };
548
641
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
549
642
  }
550
643
 
551
- protected onBufferFlushing(
552
- event: Events.BUFFER_FLUSHING,
553
- data: BufferFlushingData,
554
- ) {
555
- const { operationQueue } = this;
556
- const flushOperation = (type: SourceBufferName): BufferOperation => ({
557
- execute: this.removeExecutor.bind(
558
- this,
559
- type,
560
- data.startOffset,
561
- data.endOffset,
562
- ),
644
+ private getFlushOp(
645
+ type: SourceBufferName,
646
+ start: number,
647
+ end: number,
648
+ ): BufferOperation {
649
+ return {
650
+ execute: () => {
651
+ this.removeExecutor(type, start, end);
652
+ },
563
653
  onStart: () => {
564
654
  // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
565
655
  },
@@ -570,13 +660,26 @@ export default class BufferController implements ComponentAPI {
570
660
  onError: (error: Error) => {
571
661
  this.warn(`Failed to remove from ${type} SourceBuffer`, error);
572
662
  },
573
- });
663
+ };
664
+ }
574
665
 
575
- if (data.type) {
576
- operationQueue.append(flushOperation(data.type), data.type);
666
+ protected onBufferFlushing(
667
+ event: Events.BUFFER_FLUSHING,
668
+ data: BufferFlushingData,
669
+ ) {
670
+ const { operationQueue } = this;
671
+ const { type, startOffset, endOffset } = data;
672
+ if (type) {
673
+ operationQueue.append(
674
+ this.getFlushOp(type, startOffset, endOffset),
675
+ type,
676
+ );
577
677
  } else {
578
- this.getSourceBufferTypes().forEach((type: SourceBufferName) => {
579
- operationQueue.append(flushOperation(type), type);
678
+ this.getSourceBufferTypes().forEach((sbType: SourceBufferName) => {
679
+ operationQueue.append(
680
+ this.getFlushOp(sbType, startOffset, endOffset),
681
+ sbType,
682
+ );
580
683
  });
581
684
  }
582
685
  }
@@ -629,6 +732,9 @@ export default class BufferController implements ComponentAPI {
629
732
  // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
630
733
  // an undefined data.type will mark all buffers as EOS.
631
734
  protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
735
+ if (data.type === 'video') {
736
+ this.unblockAudio();
737
+ }
632
738
  const ended = this.getSourceBufferTypes().reduce((acc, type) => {
633
739
  const sb = this.sourceBuffer[type];
634
740
  if (sb && (!data.type || data.type === type)) {
@@ -674,14 +780,34 @@ export default class BufferController implements ComponentAPI {
674
780
  return;
675
781
  }
676
782
  this.details = details;
677
-
783
+ const durationAndRange = this.getDurationAndRange();
784
+ if (!durationAndRange) {
785
+ return;
786
+ }
678
787
  if (this.getSourceBufferTypes().length) {
679
- this.blockBuffers(this.updateMediaElementDuration.bind(this));
788
+ this.blockBuffers(() => this.updateMediaSource(durationAndRange));
680
789
  } else {
681
- this.updateMediaElementDuration();
790
+ this.updateMediaSource(durationAndRange);
791
+ }
792
+ }
793
+
794
+ private onError(event: Events.ERROR, data: ErrorData) {
795
+ if (data.details === ErrorDetails.BUFFER_APPEND_ERROR && data.frag) {
796
+ const nextAutoLevel = data.errorAction?.nextAutoLevel;
797
+ if (Number.isFinite(nextAutoLevel) && nextAutoLevel !== data.frag.level) {
798
+ this.resetAppendErrors();
799
+ }
682
800
  }
683
801
  }
684
802
 
803
+ private resetAppendErrors() {
804
+ this.appendErrors = {
805
+ audio: 0,
806
+ video: 0,
807
+ audiovideo: 0,
808
+ };
809
+ }
810
+
685
811
  trimBuffers() {
686
812
  const { hls, details, media } = this;
687
813
  if (!media || details === null) {
@@ -830,14 +956,18 @@ export default class BufferController implements ComponentAPI {
830
956
  * 'liveDurationInfinity` is set to `true`
831
957
  * More details: https://github.com/video-dev/hls.js/issues/355
832
958
  */
833
- private updateMediaElementDuration() {
959
+ private getDurationAndRange(): {
960
+ duration: number;
961
+ start?: number;
962
+ end?: number;
963
+ } | null {
834
964
  if (
835
965
  !this.details ||
836
966
  !this.media ||
837
967
  !this.mediaSource ||
838
968
  this.mediaSource.readyState !== 'open'
839
969
  ) {
840
- return;
970
+ return null;
841
971
  }
842
972
  const { details, hls, media, mediaSource } = this;
843
973
  const levelDuration = details.fragments[0].start + details.totalduration;
@@ -849,31 +979,47 @@ export default class BufferController implements ComponentAPI {
849
979
  if (details.live && hls.config.liveDurationInfinity) {
850
980
  // Override duration to Infinity
851
981
  mediaSource.duration = Infinity;
852
- this.updateSeekableRange(details);
982
+ const len = details.fragments.length;
983
+ if (len && details.live && !!mediaSource.setLiveSeekableRange) {
984
+ const start = Math.max(0, details.fragments[0].start);
985
+ const end = Math.max(start, start + details.totalduration);
986
+ return { duration: Infinity, start, end };
987
+ }
988
+ return { duration: Infinity };
853
989
  } else if (
854
990
  (levelDuration > msDuration && levelDuration > mediaDuration) ||
855
991
  !Number.isFinite(mediaDuration)
856
992
  ) {
857
- // levelDuration was the last value we set.
858
- // not using mediaSource.duration as the browser may tweak this value
859
- // only update Media Source duration if its value increase, this is to avoid
860
- // flushing already buffered portion when switching between quality level
861
- this.log(`Updating Media Source duration to ${levelDuration.toFixed(3)}`);
862
- mediaSource.duration = levelDuration;
993
+ return { duration: levelDuration };
863
994
  }
995
+ return null;
864
996
  }
865
997
 
866
- updateSeekableRange(levelDetails) {
867
- const mediaSource = this.mediaSource;
868
- const fragments = levelDetails.fragments;
869
- const len = fragments.length;
870
- if (len && levelDetails.live && mediaSource?.setLiveSeekableRange) {
871
- const start = Math.max(0, fragments[0].start);
872
- const end = Math.max(start, start + levelDetails.totalduration);
998
+ private updateMediaSource({
999
+ duration,
1000
+ start,
1001
+ end,
1002
+ }: {
1003
+ duration: number;
1004
+ start?: number;
1005
+ end?: number;
1006
+ }) {
1007
+ if (
1008
+ !this.media ||
1009
+ !this.mediaSource ||
1010
+ this.mediaSource.readyState !== 'open'
1011
+ ) {
1012
+ return;
1013
+ }
1014
+ if (Number.isFinite(duration)) {
1015
+ this.log(`Updating Media Source duration to ${duration.toFixed(3)}`);
1016
+ }
1017
+ this.mediaSource.duration = duration;
1018
+ if (start !== undefined && end !== undefined) {
873
1019
  this.log(
874
- `Media Source duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
1020
+ `Media Source duration is set to ${this.mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
875
1021
  );
876
- mediaSource.setLiveSeekableRange(start, end);
1022
+ this.mediaSource.setLiveSeekableRange(start, end);
877
1023
  }
878
1024
  }
879
1025
 
@@ -994,7 +1140,10 @@ export default class BufferController implements ComponentAPI {
994
1140
  this.log('Media source opened');
995
1141
  if (media) {
996
1142
  media.removeEventListener('emptied', this._onMediaEmptied);
997
- this.updateMediaElementDuration();
1143
+ const durationAndRange = this.getDurationAndRange();
1144
+ if (durationAndRange) {
1145
+ this.updateMediaSource(durationAndRange);
1146
+ }
998
1147
  this.hls.trigger(Events.MEDIA_ATTACHED, {
999
1148
  media,
1000
1149
  mediaSource: mediaSource as MediaSource,
@@ -1019,7 +1168,7 @@ export default class BufferController implements ComponentAPI {
1019
1168
  private _onMediaEmptied = () => {
1020
1169
  const { mediaSrc, _objectUrl } = this;
1021
1170
  if (mediaSrc !== _objectUrl) {
1022
- logger.error(
1171
+ this.error(
1023
1172
  `Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`,
1024
1173
  );
1025
1174
  }
@@ -1115,7 +1264,7 @@ export default class BufferController implements ComponentAPI {
1115
1264
  }
1116
1265
  return;
1117
1266
  }
1118
-
1267
+ sb.ending = false;
1119
1268
  sb.ended = false;
1120
1269
  sb.appendBuffer(data);
1121
1270
  }
@@ -1138,10 +1287,14 @@ export default class BufferController implements ComponentAPI {
1138
1287
  const blockingOperations = buffers.map((type) =>
1139
1288
  operationQueue.appendBlocker(type as SourceBufferName),
1140
1289
  );
1141
- Promise.all(blockingOperations).then(() => {
1290
+ const audioBlocked = buffers.length > 1 && !!this.blockedAudioAppend;
1291
+ if (audioBlocked) {
1292
+ this.unblockAudio();
1293
+ }
1294
+ Promise.all(blockingOperations).then((result) => {
1142
1295
  // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
1143
1296
  onUnblocked();
1144
- buffers.forEach((type) => {
1297
+ buffers.forEach((type, i) => {
1145
1298
  const sb = this.sourceBuffer[type];
1146
1299
  // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
1147
1300
  // 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
  }
@@ -12,7 +12,6 @@ import type {
12
12
  LevelsUpdatedData,
13
13
  } from '../types/events';
14
14
  import StreamController from './stream-controller';
15
- import { logger } from '../utils/logger';
16
15
  import type { ComponentAPI } from '../types/component-api';
17
16
  import type Hls from '../hls';
18
17
 
@@ -139,6 +138,7 @@ class CapLevelController implements ComponentAPI {
139
138
 
140
139
  protected onMediaDetaching() {
141
140
  this.stopCapping();
141
+ this.media = null;
142
142
  }
143
143
 
144
144
  detectPlayerSize() {
@@ -152,12 +152,13 @@ class CapLevelController implements ComponentAPI {
152
152
  const hls = this.hls;
153
153
  const maxLevel = this.getMaxLevel(levels.length - 1);
154
154
  if (maxLevel !== this.autoLevelCapping) {
155
- logger.log(
155
+ hls.logger.log(
156
156
  `Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`,
157
157
  );
158
158
  }
159
159
  hls.autoLevelCapping = maxLevel;
160
160
  if (
161
+ hls.autoLevelEnabled &&
161
162
  hls.autoLevelCapping > this.autoLevelCapping &&
162
163
  this.streamController
163
164
  ) {