hls.js 1.5.7 → 1.5.8-0.canary.10046

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 (72) hide show
  1. package/README.md +2 -1
  2. package/dist/hls-demo.js +10 -0
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +2314 -1298
  5. package/dist/hls.js.d.ts +97 -84
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +1486 -1075
  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 +1195 -789
  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 +1979 -982
  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 +22 -22
  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 +20 -8
  25. package/src/controller/base-stream-controller.ts +157 -36
  26. package/src/controller/buffer-controller.ts +203 -67
  27. package/src/controller/buffer-operation-queue.ts +16 -19
  28. package/src/controller/cap-level-controller.ts +2 -2
  29. package/src/controller/cmcd-controller.ts +27 -6
  30. package/src/controller/content-steering-controller.ts +8 -6
  31. package/src/controller/eme-controller.ts +9 -22
  32. package/src/controller/error-controller.ts +6 -8
  33. package/src/controller/fps-controller.ts +2 -3
  34. package/src/controller/fragment-tracker.ts +15 -11
  35. package/src/controller/gap-controller.ts +43 -16
  36. package/src/controller/latency-controller.ts +9 -11
  37. package/src/controller/level-controller.ts +12 -18
  38. package/src/controller/stream-controller.ts +36 -31
  39. package/src/controller/subtitle-stream-controller.ts +28 -40
  40. package/src/controller/subtitle-track-controller.ts +5 -3
  41. package/src/controller/timeline-controller.ts +23 -30
  42. package/src/crypt/aes-crypto.ts +21 -2
  43. package/src/crypt/decrypter-aes-mode.ts +4 -0
  44. package/src/crypt/decrypter.ts +32 -18
  45. package/src/crypt/fast-aes-key.ts +24 -5
  46. package/src/demux/audio/adts.ts +9 -4
  47. package/src/demux/sample-aes.ts +2 -0
  48. package/src/demux/transmuxer-interface.ts +4 -12
  49. package/src/demux/transmuxer-worker.ts +4 -4
  50. package/src/demux/transmuxer.ts +16 -3
  51. package/src/demux/tsdemuxer.ts +71 -37
  52. package/src/demux/video/avc-video-parser.ts +208 -119
  53. package/src/demux/video/base-video-parser.ts +134 -2
  54. package/src/demux/video/exp-golomb.ts +0 -208
  55. package/src/demux/video/hevc-video-parser.ts +746 -0
  56. package/src/events.ts +7 -0
  57. package/src/hls.ts +49 -37
  58. package/src/loader/fragment-loader.ts +9 -2
  59. package/src/loader/key-loader.ts +2 -0
  60. package/src/loader/level-key.ts +10 -9
  61. package/src/loader/playlist-loader.ts +4 -5
  62. package/src/remux/mp4-generator.ts +196 -1
  63. package/src/remux/mp4-remuxer.ts +23 -7
  64. package/src/task-loop.ts +5 -2
  65. package/src/types/component-api.ts +2 -0
  66. package/src/types/demuxer.ts +3 -0
  67. package/src/types/events.ts +4 -0
  68. package/src/utils/buffer-helper.ts +12 -31
  69. package/src/utils/codecs.ts +34 -5
  70. package/src/utils/encryption-methods-util.ts +21 -0
  71. package/src/utils/logger.ts +54 -24
  72. package/src/utils/mp4-tools.ts +4 -2
@@ -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 {
@@ -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
 
@@ -42,7 +48,7 @@ interface BufferedChangeEvent extends Event {
42
48
  readonly removedRanges?: TimeRanges;
43
49
  }
44
50
 
45
- export default class BufferController implements ComponentAPI {
51
+ export default class BufferController extends Logger implements ComponentAPI {
46
52
  // The level details used to determine duration, target-duration and live
47
53
  private details: LevelDetails | null = null;
48
54
  // cache the self generated object url to detect hijack of video tag
@@ -53,6 +59,7 @@ export default class BufferController 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 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,20 +97,14 @@ export default class BufferController implements ComponentAPI {
82
97
  public pendingTracks: TrackSet = {};
83
98
  public sourceBuffer!: SourceBuffers;
84
99
 
85
- protected log: (msg: any) => void;
86
- protected warn: (msg: any, obj?: any) => void;
87
- protected error: (msg: any, obj?: any) => void;
88
-
89
- constructor(hls: Hls) {
100
+ constructor(hls: Hls, fragmentTracker: FragmentTracker) {
101
+ super('buffer-controller', hls.logger);
90
102
  this.hls = hls;
91
- const logPrefix = '[buffer-controller]';
103
+ this.fragmentTracker = fragmentTracker;
92
104
  this.appendSource =
93
105
  hls.config.preferManagedMediaSource &&
94
106
  typeof self !== 'undefined' &&
95
107
  (self as any).ManagedMediaSource;
96
- this.log = logger.log.bind(logger, logPrefix);
97
- this.warn = logger.warn.bind(logger, logPrefix);
98
- this.error = logger.error.bind(logger, logPrefix);
99
108
  this._initSourceBuffer();
100
109
  this.registerListeners();
101
110
  }
@@ -112,7 +121,13 @@ export default class BufferController implements ComponentAPI {
112
121
  this.details = null;
113
122
  this.lastMpegAudioChunk = null;
114
123
  // @ts-ignore
115
- this.hls = null;
124
+ this.hls = this.fragmentTracker = null;
125
+ // @ts-ignore
126
+ this._onMediaSourceOpen = this._onMediaSourceClose = null;
127
+ // @ts-ignore
128
+ this._onMediaSourceEnded = null;
129
+ // @ts-ignore
130
+ this._onStartStreaming = this._onEndStreaming = null;
116
131
  }
117
132
 
118
133
  protected registerListeners() {
@@ -161,6 +176,8 @@ export default class BufferController implements ComponentAPI {
161
176
  audiovideo: 0,
162
177
  };
163
178
  this.lastMpegAudioChunk = null;
179
+ this.blockedAudioAppend = null;
180
+ this.lastVideoAppendEnd = 0;
164
181
  }
165
182
 
166
183
  private onManifestLoading() {
@@ -306,6 +323,7 @@ export default class BufferController implements ComponentAPI {
306
323
  this.resetBuffer(type);
307
324
  });
308
325
  this._initSourceBuffer();
326
+ this.hls.resumeBuffering();
309
327
  }
310
328
 
311
329
  private resetBuffer(type: SourceBufferName) {
@@ -331,11 +349,11 @@ export default class BufferController implements ComponentAPI {
331
349
  ) {
332
350
  const sourceBufferCount = this.getSourceBufferTypes().length;
333
351
  const trackNames = Object.keys(data);
334
- trackNames.forEach((trackName) => {
352
+ trackNames.forEach((trackName: SourceBufferName) => {
335
353
  if (sourceBufferCount) {
336
354
  // check if SourceBuffer codec needs to change
337
355
  const track = this.tracks[trackName];
338
- if (track && typeof track.buffer.changeType === 'function') {
356
+ if (track && typeof track.buffer?.changeType === 'function') {
339
357
  const { id, codec, levelCodec, container, metadata } =
340
358
  data[trackName];
341
359
  const currentCodecFull = pickMostCompleteCodecName(
@@ -399,7 +417,7 @@ export default class BufferController implements ComponentAPI {
399
417
  }
400
418
  }
401
419
 
402
- protected appendChangeType(type, mimeType) {
420
+ protected appendChangeType(type: SourceBufferName, mimeType: string) {
403
421
  const { operationQueue } = this;
404
422
  const operation: BufferOperation = {
405
423
  execute: () => {
@@ -420,14 +438,52 @@ export default class BufferController implements ComponentAPI {
420
438
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
421
439
  }
422
440
 
441
+ private blockAudio(partOrFrag: Fragment | Part) {
442
+ const pStart = partOrFrag.start;
443
+ const pTime = pStart + partOrFrag.duration * 0.05;
444
+ const atGap =
445
+ this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)
446
+ ?.gap === true;
447
+ if (atGap) {
448
+ return;
449
+ }
450
+ const op: BufferOperation = {
451
+ execute: () => {
452
+ if (
453
+ this.lastVideoAppendEnd > pTime ||
454
+ (this.sourceBuffer.video &&
455
+ BufferHelper.isBuffered(this.sourceBuffer.video, pTime)) ||
456
+ this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)
457
+ ?.gap === true
458
+ ) {
459
+ this.blockedAudioAppend = null;
460
+ this.operationQueue.shiftAndExecuteNext('audio');
461
+ }
462
+ },
463
+ onStart: () => {},
464
+ onComplete: () => {},
465
+ onError: () => {},
466
+ };
467
+ this.blockedAudioAppend = { op, frag: partOrFrag };
468
+ this.operationQueue.append(op, 'audio', true);
469
+ }
470
+
471
+ private unblockAudio() {
472
+ const blockedAudioAppend = this.blockedAudioAppend;
473
+ if (blockedAudioAppend) {
474
+ this.blockedAudioAppend = null;
475
+ this.operationQueue.unblockAudio(blockedAudioAppend.op);
476
+ }
477
+ }
478
+
423
479
  protected onBufferAppending(
424
480
  event: Events.BUFFER_APPENDING,
425
481
  eventData: BufferAppendingData,
426
482
  ) {
427
- const { hls, operationQueue, tracks } = this;
428
- const { data, type, frag, part, chunkMeta } = eventData;
483
+ const { operationQueue, tracks } = this;
484
+ const { data, type, parent, frag, part, chunkMeta } = eventData;
429
485
  const chunkStats = chunkMeta.buffering[type];
430
-
486
+ const sn = frag.sn;
431
487
  const bufferAppendingStart = self.performance.now();
432
488
  chunkStats.start = bufferAppendingStart;
433
489
  const fragBuffering = frag.stats.buffering;
@@ -454,7 +510,44 @@ export default class BufferController implements ComponentAPI {
454
510
  this.lastMpegAudioChunk = chunkMeta;
455
511
  }
456
512
 
457
- const fragStart = frag.start;
513
+ // Block audio append until overlapping video append
514
+ const videoSb = this.sourceBuffer.video;
515
+ if (videoSb && sn !== 'initSegment') {
516
+ const partOrFrag = part || frag;
517
+ const blockedAudioAppend = this.blockedAudioAppend;
518
+ if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) {
519
+ const pStart = partOrFrag.start;
520
+ const pTime = pStart + partOrFrag.duration * 0.05;
521
+ const vbuffered = videoSb.buffered;
522
+ const vappending = this.operationQueue.current('video');
523
+ if (!vbuffered.length && !vappending) {
524
+ // wait for video before appending audio
525
+ this.blockAudio(partOrFrag);
526
+ } else if (
527
+ !vappending &&
528
+ !BufferHelper.isBuffered(videoSb, pTime) &&
529
+ this.lastVideoAppendEnd < pTime
530
+ ) {
531
+ // audio is ahead of video
532
+ this.blockAudio(partOrFrag);
533
+ }
534
+ } else if (type === 'video') {
535
+ const videoAppendEnd = partOrFrag.end;
536
+ if (blockedAudioAppend) {
537
+ const audioStart = blockedAudioAppend.frag.start;
538
+ if (
539
+ videoAppendEnd > audioStart ||
540
+ videoAppendEnd < this.lastVideoAppendEnd ||
541
+ BufferHelper.isBuffered(videoSb, audioStart)
542
+ ) {
543
+ this.unblockAudio();
544
+ }
545
+ }
546
+ this.lastVideoAppendEnd = videoAppendEnd;
547
+ }
548
+ }
549
+
550
+ const fragStart = (part || frag).start;
458
551
  const operation: BufferOperation = {
459
552
  execute: () => {
460
553
  chunkStats.executeStart = self.performance.now();
@@ -464,7 +557,7 @@ export default class BufferController implements ComponentAPI {
464
557
  const delta = fragStart - sb.timestampOffset;
465
558
  if (Math.abs(delta) >= 0.1) {
466
559
  this.log(
467
- `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`,
560
+ `Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${sn})`,
468
561
  );
469
562
  sb.timestampOffset = fragStart;
470
563
  }
@@ -533,30 +626,27 @@ export default class BufferController implements ComponentAPI {
533
626
  browser is able to evict some data from sourcebuffer. Retrying can help recover.
534
627
  */
535
628
  this.warn(
536
- `Failed ${appendErrorCount}/${hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
629
+ `Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`,
537
630
  );
538
- if (appendErrorCount >= hls.config.appendErrorMaxRetry) {
631
+ if (appendErrorCount >= this.hls.config.appendErrorMaxRetry) {
539
632
  event.fatal = true;
540
633
  }
541
634
  }
542
- hls.trigger(Events.ERROR, event);
635
+ this.hls.trigger(Events.ERROR, event);
543
636
  },
544
637
  };
545
638
  operationQueue.append(operation, type, !!this.pendingTracks[type]);
546
639
  }
547
640
 
548
- protected onBufferFlushing(
549
- event: Events.BUFFER_FLUSHING,
550
- data: BufferFlushingData,
551
- ) {
552
- const { operationQueue } = this;
553
- const flushOperation = (type: SourceBufferName): BufferOperation => ({
554
- execute: this.removeExecutor.bind(
555
- this,
556
- type,
557
- data.startOffset,
558
- data.endOffset,
559
- ),
641
+ private getFlushOp(
642
+ type: SourceBufferName,
643
+ start: number,
644
+ end: number,
645
+ ): BufferOperation {
646
+ return {
647
+ execute: () => {
648
+ this.removeExecutor(type, start, end);
649
+ },
560
650
  onStart: () => {
561
651
  // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
562
652
  },
@@ -567,13 +657,26 @@ export default class BufferController implements ComponentAPI {
567
657
  onError: (error: Error) => {
568
658
  this.warn(`Failed to remove from ${type} SourceBuffer`, error);
569
659
  },
570
- });
660
+ };
661
+ }
571
662
 
572
- if (data.type) {
573
- operationQueue.append(flushOperation(data.type), data.type);
663
+ protected onBufferFlushing(
664
+ event: Events.BUFFER_FLUSHING,
665
+ data: BufferFlushingData,
666
+ ) {
667
+ const { operationQueue } = this;
668
+ const { type, startOffset, endOffset } = data;
669
+ if (type) {
670
+ operationQueue.append(
671
+ this.getFlushOp(type, startOffset, endOffset),
672
+ type,
673
+ );
574
674
  } else {
575
- this.getSourceBufferTypes().forEach((type: SourceBufferName) => {
576
- operationQueue.append(flushOperation(type), type);
675
+ this.getSourceBufferTypes().forEach((sbType: SourceBufferName) => {
676
+ operationQueue.append(
677
+ this.getFlushOp(sbType, startOffset, endOffset),
678
+ sbType,
679
+ );
577
680
  });
578
681
  }
579
682
  }
@@ -626,6 +729,9 @@ export default class BufferController implements ComponentAPI {
626
729
  // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
627
730
  // an undefined data.type will mark all buffers as EOS.
628
731
  protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
732
+ if (data.type === 'video') {
733
+ this.unblockAudio();
734
+ }
629
735
  const ended = this.getSourceBufferTypes().reduce((acc, type) => {
630
736
  const sb = this.sourceBuffer[type];
631
737
  if (sb && (!data.type || data.type === type)) {
@@ -671,11 +777,14 @@ export default class BufferController implements ComponentAPI {
671
777
  return;
672
778
  }
673
779
  this.details = details;
674
-
780
+ const durationAndRange = this.getDurationAndRange();
781
+ if (!durationAndRange) {
782
+ return;
783
+ }
675
784
  if (this.getSourceBufferTypes().length) {
676
- this.blockBuffers(this.updateMediaElementDuration.bind(this));
785
+ this.blockBuffers(() => this.updateMediaSource(durationAndRange));
677
786
  } else {
678
- this.updateMediaElementDuration();
787
+ this.updateMediaSource(durationAndRange);
679
788
  }
680
789
  }
681
790
 
@@ -827,14 +936,18 @@ export default class BufferController implements ComponentAPI {
827
936
  * 'liveDurationInfinity` is set to `true`
828
937
  * More details: https://github.com/video-dev/hls.js/issues/355
829
938
  */
830
- private updateMediaElementDuration() {
939
+ private getDurationAndRange(): {
940
+ duration: number;
941
+ start?: number;
942
+ end?: number;
943
+ } | null {
831
944
  if (
832
945
  !this.details ||
833
946
  !this.media ||
834
947
  !this.mediaSource ||
835
948
  this.mediaSource.readyState !== 'open'
836
949
  ) {
837
- return;
950
+ return null;
838
951
  }
839
952
  const { details, hls, media, mediaSource } = this;
840
953
  const levelDuration = details.fragments[0].start + details.totalduration;
@@ -846,31 +959,47 @@ export default class BufferController implements ComponentAPI {
846
959
  if (details.live && hls.config.liveDurationInfinity) {
847
960
  // Override duration to Infinity
848
961
  mediaSource.duration = Infinity;
849
- this.updateSeekableRange(details);
962
+ const len = details.fragments.length;
963
+ if (len && details.live && !!mediaSource.setLiveSeekableRange) {
964
+ const start = Math.max(0, details.fragments[0].start);
965
+ const end = Math.max(start, start + details.totalduration);
966
+ return { duration: Infinity, start, end };
967
+ }
968
+ return { duration: Infinity };
850
969
  } else if (
851
970
  (levelDuration > msDuration && levelDuration > mediaDuration) ||
852
971
  !Number.isFinite(mediaDuration)
853
972
  ) {
854
- // levelDuration was the last value we set.
855
- // not using mediaSource.duration as the browser may tweak this value
856
- // only update Media Source duration if its value increase, this is to avoid
857
- // flushing already buffered portion when switching between quality level
858
- this.log(`Updating Media Source duration to ${levelDuration.toFixed(3)}`);
859
- mediaSource.duration = levelDuration;
973
+ return { duration: levelDuration };
860
974
  }
975
+ return null;
861
976
  }
862
977
 
863
- updateSeekableRange(levelDetails) {
864
- const mediaSource = this.mediaSource;
865
- const fragments = levelDetails.fragments;
866
- const len = fragments.length;
867
- if (len && levelDetails.live && mediaSource?.setLiveSeekableRange) {
868
- const start = Math.max(0, fragments[0].start);
869
- const end = Math.max(start, start + levelDetails.totalduration);
978
+ private updateMediaSource({
979
+ duration,
980
+ start,
981
+ end,
982
+ }: {
983
+ duration: number;
984
+ start?: number;
985
+ end?: number;
986
+ }) {
987
+ if (
988
+ !this.media ||
989
+ !this.mediaSource ||
990
+ this.mediaSource.readyState !== 'open'
991
+ ) {
992
+ return;
993
+ }
994
+ if (Number.isFinite(duration)) {
995
+ this.log(`Updating Media Source duration to ${duration.toFixed(3)}`);
996
+ }
997
+ this.mediaSource.duration = duration;
998
+ if (start !== undefined && end !== undefined) {
870
999
  this.log(
871
- `Media Source duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
1000
+ `Media Source duration is set to ${this.mediaSource.duration}. Setting seekable range to ${start}-${end}.`,
872
1001
  );
873
- mediaSource.setLiveSeekableRange(start, end);
1002
+ this.mediaSource.setLiveSeekableRange(start, end);
874
1003
  }
875
1004
  }
876
1005
 
@@ -988,7 +1117,10 @@ export default class BufferController implements ComponentAPI {
988
1117
  this.log('Media source opened');
989
1118
  if (media) {
990
1119
  media.removeEventListener('emptied', this._onMediaEmptied);
991
- this.updateMediaElementDuration();
1120
+ const durationAndRange = this.getDurationAndRange();
1121
+ if (durationAndRange) {
1122
+ this.updateMediaSource(durationAndRange);
1123
+ }
992
1124
  this.hls.trigger(Events.MEDIA_ATTACHED, {
993
1125
  media,
994
1126
  mediaSource: mediaSource as MediaSource,
@@ -1013,7 +1145,7 @@ export default class BufferController implements ComponentAPI {
1013
1145
  private _onMediaEmptied = () => {
1014
1146
  const { mediaSrc, _objectUrl } = this;
1015
1147
  if (mediaSrc !== _objectUrl) {
1016
- logger.error(
1148
+ this.error(
1017
1149
  `Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`,
1018
1150
  );
1019
1151
  }
@@ -1109,7 +1241,7 @@ export default class BufferController implements ComponentAPI {
1109
1241
  }
1110
1242
  return;
1111
1243
  }
1112
-
1244
+ sb.ending = false;
1113
1245
  sb.ended = false;
1114
1246
  sb.appendBuffer(data);
1115
1247
  }
@@ -1132,10 +1264,14 @@ export default class BufferController implements ComponentAPI {
1132
1264
  const blockingOperations = buffers.map((type) =>
1133
1265
  operationQueue.appendBlocker(type as SourceBufferName),
1134
1266
  );
1135
- Promise.all(blockingOperations).then(() => {
1267
+ const audioBlocked = buffers.length > 1 && !!this.blockedAudioAppend;
1268
+ if (audioBlocked) {
1269
+ this.unblockAudio();
1270
+ }
1271
+ Promise.all(blockingOperations).then((result) => {
1136
1272
  // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
1137
1273
  onUnblocked();
1138
- buffers.forEach((type) => {
1274
+ buffers.forEach((type, i) => {
1139
1275
  const sb = this.sourceBuffer[type];
1140
1276
  // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
1141
1277
  // 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
 
@@ -152,12 +151,13 @@ class CapLevelController implements ComponentAPI {
152
151
  const hls = this.hls;
153
152
  const maxLevel = this.getMaxLevel(levels.length - 1);
154
153
  if (maxLevel !== this.autoLevelCapping) {
155
- logger.log(
154
+ hls.logger.log(
156
155
  `Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`,
157
156
  );
158
157
  }
159
158
  hls.autoLevelCapping = maxLevel;
160
159
  if (
160
+ hls.autoLevelEnabled &&
161
161
  hls.autoLevelCapping > this.autoLevelCapping &&
162
162
  this.streamController
163
163
  ) {
@@ -5,9 +5,9 @@ import { CmcdObjectType } from '@svta/common-media-library/cmcd/CmcdObjectType';
5
5
  import { CmcdStreamingFormat } from '@svta/common-media-library/cmcd/CmcdStreamingFormat';
6
6
  import { appendCmcdHeaders } from '@svta/common-media-library/cmcd/appendCmcdHeaders';
7
7
  import { appendCmcdQuery } from '@svta/common-media-library/cmcd/appendCmcdQuery';
8
+ import type { CmcdEncodeOptions } from '@svta/common-media-library/cmcd/CmcdEncodeOptions';
8
9
  import { uuid } from '@svta/common-media-library/utils/uuid';
9
10
  import { BufferHelper } from '../utils/buffer-helper';
10
- import { logger } from '../utils/logger';
11
11
  import type { ComponentAPI } from '../types/component-api';
12
12
  import type { Fragment } from '../loader/fragment';
13
13
  import type { BufferCreatedData, MediaAttachedData } from '../types/events';
@@ -165,7 +165,7 @@ export default class CMCDController implements ComponentAPI {
165
165
  data.su = this.buffering;
166
166
  }
167
167
 
168
- // TODO: Implement rtp, nrr, nor, dl
168
+ // TODO: Implement rtp, nrr, dl
169
169
 
170
170
  const { includeKeys } = this;
171
171
  if (includeKeys) {
@@ -175,14 +175,18 @@ export default class CMCDController implements ComponentAPI {
175
175
  }, {});
176
176
  }
177
177
 
178
+ const options: CmcdEncodeOptions = {
179
+ baseUrl: context.url,
180
+ };
181
+
178
182
  if (this.useHeaders) {
179
183
  if (!context.headers) {
180
184
  context.headers = {};
181
185
  }
182
186
 
183
- appendCmcdHeaders(context.headers, data);
187
+ appendCmcdHeaders(context.headers, data, options);
184
188
  } else {
185
- context.url = appendCmcdQuery(context.url, data);
189
+ context.url = appendCmcdQuery(context.url, data, options);
186
190
  }
187
191
  }
188
192
 
@@ -196,7 +200,7 @@ export default class CMCDController implements ComponentAPI {
196
200
  su: !this.initialized,
197
201
  });
198
202
  } catch (error) {
199
- logger.warn('Could not generate manifest CMCD data.', error);
203
+ this.hls.logger.warn('Could not generate manifest CMCD data.', error);
200
204
  }
201
205
  };
202
206
 
@@ -223,12 +227,29 @@ export default class CMCDController implements ComponentAPI {
223
227
  data.bl = this.getBufferLength(ot);
224
228
  }
225
229
 
230
+ const next = this.getNextFrag(fragment);
231
+ if (next) {
232
+ if (next.url && next.url !== fragment.url) {
233
+ data.nor = next.url;
234
+ }
235
+ }
236
+
226
237
  this.apply(context, data);
227
238
  } catch (error) {
228
- logger.warn('Could not generate segment CMCD data.', error);
239
+ this.hls.logger.warn('Could not generate segment CMCD data.', error);
229
240
  }
230
241
  };
231
242
 
243
+ private getNextFrag(fragment: Fragment): Fragment | undefined {
244
+ const levelDetails = this.hls.levels[fragment.level]?.details;
245
+ if (levelDetails) {
246
+ const index = (fragment.sn as number) - levelDetails.startSN;
247
+ return levelDetails.fragments[index + 1];
248
+ }
249
+
250
+ return undefined;
251
+ }
252
+
232
253
  /**
233
254
  * The CMCD object type.
234
255
  */
@@ -3,7 +3,7 @@ import { Level } from '../types/level';
3
3
  import { reassignFragmentLevelIndexes } from '../utils/level-helper';
4
4
  import { AttrList } from '../utils/attr-list';
5
5
  import { ErrorActionFlags, NetworkErrorAction } from './error-controller';
6
- import { logger } from '../utils/logger';
6
+ import { Logger } from '../utils/logger';
7
7
  import {
8
8
  PlaylistContextType,
9
9
  type Loader,
@@ -48,9 +48,11 @@ export type UriReplacement = {
48
48
 
49
49
  const PATHWAY_PENALTY_DURATION_MS = 300000;
50
50
 
51
- export default class ContentSteeringController implements NetworkComponentAPI {
51
+ export default class ContentSteeringController
52
+ extends Logger
53
+ implements NetworkComponentAPI
54
+ {
52
55
  private readonly hls: Hls;
53
- private log: (msg: any) => void;
54
56
  private loader: Loader<LoaderContext> | null = null;
55
57
  private uri: string | null = null;
56
58
  private pathwayId: string = '.';
@@ -66,8 +68,8 @@ export default class ContentSteeringController implements NetworkComponentAPI {
66
68
  private penalizedPathways: { [pathwayId: string]: number } = {};
67
69
 
68
70
  constructor(hls: Hls) {
71
+ super('content-steering', hls.logger);
69
72
  this.hls = hls;
70
- this.log = logger.log.bind(logger, `[content-steering]:`);
71
73
  this.registerListeners();
72
74
  }
73
75
 
@@ -203,7 +205,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
203
205
  errorAction.resolved = this.pathwayId !== errorPathway;
204
206
  }
205
207
  if (!errorAction.resolved) {
206
- logger.warn(
208
+ this.warn(
207
209
  `Could not resolve ${data.details} ("${
208
210
  data.error.message
209
211
  }") with content-steering for Pathway: ${errorPathway} levels: ${
@@ -442,7 +444,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
442
444
  ) => {
443
445
  this.log(`Loaded steering manifest: "${url}"`);
444
446
  const steeringData = response.data as SteeringManifest;
445
- if (steeringData.VERSION !== 1) {
447
+ if (steeringData?.VERSION !== 1) {
446
448
  this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
447
449
  return;
448
450
  }