hls.js 1.5.11 → 1.5.12-0.canary.10340

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 (88) 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 +3477 -2195
  5. package/dist/hls.js.d.ts +108 -85
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2401 -1754
  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 +2148 -1476
  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 +2863 -1558
  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 +36 -36
  20. package/src/config.ts +3 -2
  21. package/src/controller/abr-controller.ts +24 -20
  22. package/src/controller/audio-stream-controller.ts +68 -74
  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 +160 -38
  26. package/src/controller/buffer-controller.ts +230 -92
  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 +6 -8
  33. package/src/controller/fps-controller.ts +8 -3
  34. package/src/controller/fragment-tracker.ts +15 -11
  35. package/src/controller/gap-controller.ts +43 -16
  36. package/src/controller/id3-track-controller.ts +7 -7
  37. package/src/controller/latency-controller.ts +9 -11
  38. package/src/controller/level-controller.ts +37 -19
  39. package/src/controller/stream-controller.ts +37 -32
  40. package/src/controller/subtitle-stream-controller.ts +28 -40
  41. package/src/controller/subtitle-track-controller.ts +5 -3
  42. package/src/controller/timeline-controller.ts +19 -21
  43. package/src/crypt/aes-crypto.ts +21 -2
  44. package/src/crypt/decrypter-aes-mode.ts +4 -0
  45. package/src/crypt/decrypter.ts +32 -16
  46. package/src/crypt/fast-aes-key.ts +28 -5
  47. package/src/demux/audio/aacdemuxer.ts +2 -2
  48. package/src/demux/audio/ac3-demuxer.ts +4 -3
  49. package/src/demux/audio/adts.ts +9 -4
  50. package/src/demux/audio/base-audio-demuxer.ts +16 -14
  51. package/src/demux/audio/mp3demuxer.ts +4 -3
  52. package/src/demux/audio/mpegaudio.ts +1 -1
  53. package/src/demux/mp4demuxer.ts +7 -7
  54. package/src/demux/sample-aes.ts +2 -0
  55. package/src/demux/transmuxer-interface.ts +4 -12
  56. package/src/demux/transmuxer-worker.ts +4 -4
  57. package/src/demux/transmuxer.ts +16 -3
  58. package/src/demux/tsdemuxer.ts +75 -38
  59. package/src/demux/video/avc-video-parser.ts +208 -119
  60. package/src/demux/video/base-video-parser.ts +147 -18
  61. package/src/demux/video/exp-golomb.ts +0 -208
  62. package/src/demux/video/hevc-video-parser.ts +749 -0
  63. package/src/events.ts +8 -1
  64. package/src/exports-named.ts +1 -1
  65. package/src/hls.ts +61 -38
  66. package/src/loader/fragment-loader.ts +10 -3
  67. package/src/loader/key-loader.ts +3 -1
  68. package/src/loader/level-key.ts +10 -9
  69. package/src/loader/playlist-loader.ts +4 -5
  70. package/src/remux/mp4-generator.ts +196 -1
  71. package/src/remux/mp4-remuxer.ts +24 -8
  72. package/src/task-loop.ts +5 -2
  73. package/src/types/component-api.ts +3 -1
  74. package/src/types/demuxer.ts +4 -0
  75. package/src/types/events.ts +4 -0
  76. package/src/types/remuxer.ts +1 -1
  77. package/src/utils/buffer-helper.ts +12 -31
  78. package/src/utils/cea-608-parser.ts +1 -3
  79. package/src/utils/codecs.ts +34 -5
  80. package/src/utils/encryption-methods-util.ts +21 -0
  81. package/src/utils/fetch-loader.ts +1 -1
  82. package/src/utils/imsc1-ttml-parser.ts +1 -1
  83. package/src/utils/keysystem-util.ts +1 -6
  84. package/src/utils/logger.ts +58 -23
  85. package/src/utils/mp4-tools.ts +5 -3
  86. package/src/utils/utf8-utils.ts +18 -0
  87. package/src/utils/webvtt-parser.ts +1 -1
  88. package/src/demux/id3.ts +0 -411
@@ -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() {
@@ -163,6 +179,8 @@ export default class BufferController implements ComponentAPI {
163
179
  audiovideo: 0,
164
180
  };
165
181
  this.lastMpegAudioChunk = null;
182
+ this.blockedAudioAppend = null;
183
+ this.lastVideoAppendEnd = 0;
166
184
  }
167
185
 
168
186
  private onManifestLoading() {
@@ -270,37 +288,38 @@ export default class BufferController implements ComponentAPI {
270
288
  mediaSource.removeEventListener('endstreaming', this._onEndStreaming);
271
289
  }
272
290
 
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
- }
291
+ this.mediaSource = null;
292
+ this._objectUrl = null;
293
+ }
280
294
 
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
- }
295
+ // Detach properly the MediaSource from the HTMLMediaElement as
296
+ // suggested in https://github.com/w3c/media-source/issues/53.
297
+ if (media) {
298
+ media.removeEventListener('emptied', this._onMediaEmptied);
299
+ if (_objectUrl) {
300
+ self.URL.revokeObjectURL(_objectUrl);
294
301
  }
295
302
 
296
- this.mediaSource = null;
303
+ // clean up video tag src only if it's our own url. some external libraries might
304
+ // hijack the video tag and change its 'src' without destroying the Hls instance first
305
+ if (this.mediaSrc === _objectUrl) {
306
+ media.removeAttribute('src');
307
+ if (this.appendSource) {
308
+ removeSourceChildren(media);
309
+ }
310
+ media.load();
311
+ } else {
312
+ this.warn(
313
+ 'media|source.src was changed by a third party - skip cleanup',
314
+ );
315
+ }
297
316
  this.media = null;
298
- this._objectUrl = null;
299
- this.bufferCodecEventsExpected = this._bufferCodecEventsTotal;
300
- this.pendingTracks = {};
301
- this.tracks = {};
302
317
  }
303
318
 
319
+ this.bufferCodecEventsExpected = this._bufferCodecEventsTotal;
320
+ this.pendingTracks = {};
321
+ this.tracks = {};
322
+
304
323
  this.hls.trigger(Events.MEDIA_DETACHED, undefined);
305
324
  }
306
325
 
@@ -309,6 +328,7 @@ export default class BufferController implements ComponentAPI {
309
328
  this.resetBuffer(type);
310
329
  });
311
330
  this._initSourceBuffer();
331
+ this.hls.resumeBuffering();
312
332
  }
313
333
 
314
334
  private resetBuffer(type: SourceBufferName) {
@@ -334,11 +354,11 @@ export default class BufferController implements ComponentAPI {
334
354
  ) {
335
355
  const sourceBufferCount = this.getSourceBufferTypes().length;
336
356
  const trackNames = Object.keys(data);
337
- trackNames.forEach((trackName) => {
357
+ trackNames.forEach((trackName: SourceBufferName) => {
338
358
  if (sourceBufferCount) {
339
359
  // check if SourceBuffer codec needs to change
340
360
  const track = this.tracks[trackName];
341
- if (track && typeof track.buffer.changeType === 'function') {
361
+ if (track && typeof track.buffer?.changeType === 'function') {
342
362
  const { id, codec, levelCodec, container, metadata } =
343
363
  data[trackName];
344
364
  const currentCodecFull = pickMostCompleteCodecName(
@@ -402,7 +422,7 @@ export default class BufferController implements ComponentAPI {
402
422
  }
403
423
  }
404
424
 
405
- protected appendChangeType(type, mimeType) {
425
+ protected appendChangeType(type: SourceBufferName, mimeType: string) {
406
426
  const { operationQueue } = this;
407
427
  const operation: BufferOperation = {
408
428
  execute: () => {
@@ -423,14 +443,52 @@ export default class BufferController implements ComponentAPI {
423
443
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
424
444
  }
425
445
 
446
+ private blockAudio(partOrFrag: Fragment | Part) {
447
+ const pStart = partOrFrag.start;
448
+ const pTime = pStart + partOrFrag.duration * 0.05;
449
+ const atGap =
450
+ this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)
451
+ ?.gap === true;
452
+ if (atGap) {
453
+ return;
454
+ }
455
+ const op: BufferOperation = {
456
+ execute: () => {
457
+ if (
458
+ this.lastVideoAppendEnd > pTime ||
459
+ (this.sourceBuffer.video &&
460
+ BufferHelper.isBuffered(this.sourceBuffer.video, pTime)) ||
461
+ this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)
462
+ ?.gap === true
463
+ ) {
464
+ this.blockedAudioAppend = null;
465
+ this.operationQueue.shiftAndExecuteNext('audio');
466
+ }
467
+ },
468
+ onStart: () => {},
469
+ onComplete: () => {},
470
+ onError: () => {},
471
+ };
472
+ this.blockedAudioAppend = { op, frag: partOrFrag };
473
+ this.operationQueue.append(op, 'audio', true);
474
+ }
475
+
476
+ private unblockAudio() {
477
+ const blockedAudioAppend = this.blockedAudioAppend;
478
+ if (blockedAudioAppend) {
479
+ this.blockedAudioAppend = null;
480
+ this.operationQueue.unblockAudio(blockedAudioAppend.op);
481
+ }
482
+ }
483
+
426
484
  protected onBufferAppending(
427
485
  event: Events.BUFFER_APPENDING,
428
486
  eventData: BufferAppendingData,
429
487
  ) {
430
- const { hls, operationQueue, tracks } = this;
431
- const { data, type, frag, part, chunkMeta } = eventData;
488
+ const { operationQueue, tracks } = this;
489
+ const { data, type, parent, frag, part, chunkMeta } = eventData;
432
490
  const chunkStats = chunkMeta.buffering[type];
433
-
491
+ const sn = frag.sn;
434
492
  const bufferAppendingStart = self.performance.now();
435
493
  chunkStats.start = bufferAppendingStart;
436
494
  const fragBuffering = frag.stats.buffering;
@@ -457,7 +515,44 @@ export default class BufferController implements ComponentAPI {
457
515
  this.lastMpegAudioChunk = chunkMeta;
458
516
  }
459
517
 
460
- const fragStart = frag.start;
518
+ // Block audio append until overlapping video append
519
+ const videoSb = this.sourceBuffer.video;
520
+ if (videoSb && sn !== 'initSegment') {
521
+ const partOrFrag = part || frag;
522
+ const blockedAudioAppend = this.blockedAudioAppend;
523
+ if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) {
524
+ const pStart = partOrFrag.start;
525
+ const pTime = pStart + partOrFrag.duration * 0.05;
526
+ const vbuffered = videoSb.buffered;
527
+ const vappending = this.operationQueue.current('video');
528
+ if (!vbuffered.length && !vappending) {
529
+ // wait for video before appending audio
530
+ this.blockAudio(partOrFrag);
531
+ } else if (
532
+ !vappending &&
533
+ !BufferHelper.isBuffered(videoSb, pTime) &&
534
+ this.lastVideoAppendEnd < pTime
535
+ ) {
536
+ // audio is ahead of video
537
+ this.blockAudio(partOrFrag);
538
+ }
539
+ } else if (type === 'video') {
540
+ const videoAppendEnd = partOrFrag.end;
541
+ if (blockedAudioAppend) {
542
+ const audioStart = blockedAudioAppend.frag.start;
543
+ if (
544
+ videoAppendEnd > audioStart ||
545
+ videoAppendEnd < this.lastVideoAppendEnd ||
546
+ BufferHelper.isBuffered(videoSb, audioStart)
547
+ ) {
548
+ this.unblockAudio();
549
+ }
550
+ }
551
+ this.lastVideoAppendEnd = videoAppendEnd;
552
+ }
553
+ }
554
+
555
+ const fragStart = (part || frag).start;
461
556
  const operation: BufferOperation = {
462
557
  execute: () => {
463
558
  chunkStats.executeStart = self.performance.now();
@@ -467,7 +562,7 @@ export default class BufferController implements ComponentAPI {
467
562
  const delta = fragStart - sb.timestampOffset;
468
563
  if (Math.abs(delta) >= 0.1) {
469
564
  this.log(
470
- `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`,
565
+ `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${sn})`,
471
566
  );
472
567
  sb.timestampOffset = fragStart;
473
568
  }
@@ -536,30 +631,27 @@ export default class BufferController implements ComponentAPI {
536
631
  browser is able to evict some data from sourcebuffer. Retrying can help recover.
537
632
  */
538
633
  this.warn(
539
- `Failed ${appendErrorCount}/${hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
634
+ `Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
540
635
  );
541
- if (appendErrorCount >= hls.config.appendErrorMaxRetry) {
636
+ if (appendErrorCount >= this.hls.config.appendErrorMaxRetry) {
542
637
  event.fatal = true;
543
638
  }
544
639
  }
545
- hls.trigger(Events.ERROR, event);
640
+ this.hls.trigger(Events.ERROR, event);
546
641
  },
547
642
  };
548
643
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
549
644
  }
550
645
 
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
- ),
646
+ private getFlushOp(
647
+ type: SourceBufferName,
648
+ start: number,
649
+ end: number,
650
+ ): BufferOperation {
651
+ return {
652
+ execute: () => {
653
+ this.removeExecutor(type, start, end);
654
+ },
563
655
  onStart: () => {
564
656
  // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
565
657
  },
@@ -570,13 +662,26 @@ export default class BufferController implements ComponentAPI {
570
662
  onError: (error: Error) => {
571
663
  this.warn(`Failed to remove from ${type} SourceBuffer`, error);
572
664
  },
573
- });
665
+ };
666
+ }
574
667
 
575
- if (data.type) {
576
- operationQueue.append(flushOperation(data.type), data.type);
668
+ protected onBufferFlushing(
669
+ event: Events.BUFFER_FLUSHING,
670
+ data: BufferFlushingData,
671
+ ) {
672
+ const { operationQueue } = this;
673
+ const { type, startOffset, endOffset } = data;
674
+ if (type) {
675
+ operationQueue.append(
676
+ this.getFlushOp(type, startOffset, endOffset),
677
+ type,
678
+ );
577
679
  } else {
578
- this.getSourceBufferTypes().forEach((type: SourceBufferName) => {
579
- operationQueue.append(flushOperation(type), type);
680
+ this.getSourceBufferTypes().forEach((sbType: SourceBufferName) => {
681
+ operationQueue.append(
682
+ this.getFlushOp(sbType, startOffset, endOffset),
683
+ sbType,
684
+ );
580
685
  });
581
686
  }
582
687
  }
@@ -629,6 +734,9 @@ export default class BufferController implements ComponentAPI {
629
734
  // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
630
735
  // an undefined data.type will mark all buffers as EOS.
631
736
  protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
737
+ if (data.type === 'video') {
738
+ this.unblockAudio();
739
+ }
632
740
  const ended = this.getSourceBufferTypes().reduce((acc, type) => {
633
741
  const sb = this.sourceBuffer[type];
634
742
  if (sb && (!data.type || data.type === type)) {
@@ -674,11 +782,14 @@ export default class BufferController implements ComponentAPI {
674
782
  return;
675
783
  }
676
784
  this.details = details;
677
-
785
+ const durationAndRange = this.getDurationAndRange();
786
+ if (!durationAndRange) {
787
+ return;
788
+ }
678
789
  if (this.getSourceBufferTypes().length) {
679
- this.blockBuffers(this.updateMediaElementDuration.bind(this));
790
+ this.blockBuffers(() => this.updateMediaSource(durationAndRange));
680
791
  } else {
681
- this.updateMediaElementDuration();
792
+ this.updateMediaSource(durationAndRange);
682
793
  }
683
794
  }
684
795
 
@@ -830,14 +941,18 @@ export default class BufferController implements ComponentAPI {
830
941
  * 'liveDurationInfinity` is set to `true`
831
942
  * More details: https://github.com/video-dev/hls.js/issues/355
832
943
  */
833
- private updateMediaElementDuration() {
944
+ private getDurationAndRange(): {
945
+ duration: number;
946
+ start?: number;
947
+ end?: number;
948
+ } | null {
834
949
  if (
835
950
  !this.details ||
836
951
  !this.media ||
837
952
  !this.mediaSource ||
838
953
  this.mediaSource.readyState !== 'open'
839
954
  ) {
840
- return;
955
+ return null;
841
956
  }
842
957
  const { details, hls, media, mediaSource } = this;
843
958
  const levelDuration = details.fragments[0].start + details.totalduration;
@@ -849,31 +964,47 @@ export default class BufferController implements ComponentAPI {
849
964
  if (details.live && hls.config.liveDurationInfinity) {
850
965
  // Override duration to Infinity
851
966
  mediaSource.duration = Infinity;
852
- this.updateSeekableRange(details);
967
+ const len = details.fragments.length;
968
+ if (len && details.live && !!mediaSource.setLiveSeekableRange) {
969
+ const start = Math.max(0, details.fragments[0].start);
970
+ const end = Math.max(start, start + details.totalduration);
971
+ return { duration: Infinity, start, end };
972
+ }
973
+ return { duration: Infinity };
853
974
  } else if (
854
975
  (levelDuration > msDuration && levelDuration > mediaDuration) ||
855
976
  !Number.isFinite(mediaDuration)
856
977
  ) {
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;
978
+ return { duration: levelDuration };
863
979
  }
980
+ return null;
864
981
  }
865
982
 
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);
983
+ private updateMediaSource({
984
+ duration,
985
+ start,
986
+ end,
987
+ }: {
988
+ duration: number;
989
+ start?: number;
990
+ end?: number;
991
+ }) {
992
+ if (
993
+ !this.media ||
994
+ !this.mediaSource ||
995
+ this.mediaSource.readyState !== 'open'
996
+ ) {
997
+ return;
998
+ }
999
+ if (Number.isFinite(duration)) {
1000
+ this.log(`Updating Media Source duration to ${duration.toFixed(3)}`);
1001
+ }
1002
+ this.mediaSource.duration = duration;
1003
+ if (start !== undefined && end !== undefined) {
873
1004
  this.log(
874
- `Media Source duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
1005
+ `Media Source duration is set to ${this.mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
875
1006
  );
876
- mediaSource.setLiveSeekableRange(start, end);
1007
+ this.mediaSource.setLiveSeekableRange(start, end);
877
1008
  }
878
1009
  }
879
1010
 
@@ -994,7 +1125,10 @@ export default class BufferController implements ComponentAPI {
994
1125
  this.log('Media source opened');
995
1126
  if (media) {
996
1127
  media.removeEventListener('emptied', this._onMediaEmptied);
997
- this.updateMediaElementDuration();
1128
+ const durationAndRange = this.getDurationAndRange();
1129
+ if (durationAndRange) {
1130
+ this.updateMediaSource(durationAndRange);
1131
+ }
998
1132
  this.hls.trigger(Events.MEDIA_ATTACHED, {
999
1133
  media,
1000
1134
  mediaSource: mediaSource as MediaSource,
@@ -1019,7 +1153,7 @@ export default class BufferController implements ComponentAPI {
1019
1153
  private _onMediaEmptied = () => {
1020
1154
  const { mediaSrc, _objectUrl } = this;
1021
1155
  if (mediaSrc !== _objectUrl) {
1022
- logger.error(
1156
+ this.error(
1023
1157
  `Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`,
1024
1158
  );
1025
1159
  }
@@ -1115,7 +1249,7 @@ export default class BufferController implements ComponentAPI {
1115
1249
  }
1116
1250
  return;
1117
1251
  }
1118
-
1252
+ sb.ending = false;
1119
1253
  sb.ended = false;
1120
1254
  sb.appendBuffer(data);
1121
1255
  }
@@ -1138,10 +1272,14 @@ export default class BufferController implements ComponentAPI {
1138
1272
  const blockingOperations = buffers.map((type) =>
1139
1273
  operationQueue.appendBlocker(type as SourceBufferName),
1140
1274
  );
1141
- Promise.all(blockingOperations).then(() => {
1275
+ const audioBlocked = buffers.length > 1 && !!this.blockedAudioAppend;
1276
+ if (audioBlocked) {
1277
+ this.unblockAudio();
1278
+ }
1279
+ Promise.all(blockingOperations).then((result) => {
1142
1280
  // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
1143
1281
  onUnblocked();
1144
- buffers.forEach((type) => {
1282
+ buffers.forEach((type, i) => {
1145
1283
  const sb = this.sourceBuffer[type];
1146
1284
  // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
1147
1285
  // 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
  ) {