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.
- package/README.md +2 -1
- package/dist/hls-demo.js +10 -0
- package/dist/hls-demo.js.map +1 -1
- package/dist/hls.js +2314 -1298
- package/dist/hls.js.d.ts +97 -84
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +1486 -1075
- package/dist/hls.light.js.map +1 -1
- package/dist/hls.light.min.js +1 -1
- package/dist/hls.light.min.js.map +1 -1
- package/dist/hls.light.mjs +1195 -789
- package/dist/hls.light.mjs.map +1 -1
- package/dist/hls.min.js +1 -1
- package/dist/hls.min.js.map +1 -1
- package/dist/hls.mjs +1979 -982
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/dist/hls.worker.js.map +1 -1
- package/package.json +22 -22
- package/src/config.ts +3 -2
- package/src/controller/abr-controller.ts +24 -20
- package/src/controller/audio-stream-controller.ts +68 -74
- package/src/controller/audio-track-controller.ts +1 -1
- package/src/controller/base-playlist-controller.ts +20 -8
- package/src/controller/base-stream-controller.ts +157 -36
- package/src/controller/buffer-controller.ts +203 -67
- package/src/controller/buffer-operation-queue.ts +16 -19
- package/src/controller/cap-level-controller.ts +2 -2
- package/src/controller/cmcd-controller.ts +27 -6
- package/src/controller/content-steering-controller.ts +8 -6
- package/src/controller/eme-controller.ts +9 -22
- package/src/controller/error-controller.ts +6 -8
- package/src/controller/fps-controller.ts +2 -3
- package/src/controller/fragment-tracker.ts +15 -11
- package/src/controller/gap-controller.ts +43 -16
- package/src/controller/latency-controller.ts +9 -11
- package/src/controller/level-controller.ts +12 -18
- package/src/controller/stream-controller.ts +36 -31
- package/src/controller/subtitle-stream-controller.ts +28 -40
- package/src/controller/subtitle-track-controller.ts +5 -3
- package/src/controller/timeline-controller.ts +23 -30
- package/src/crypt/aes-crypto.ts +21 -2
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +32 -18
- package/src/crypt/fast-aes-key.ts +24 -5
- package/src/demux/audio/adts.ts +9 -4
- package/src/demux/sample-aes.ts +2 -0
- package/src/demux/transmuxer-interface.ts +4 -12
- package/src/demux/transmuxer-worker.ts +4 -4
- package/src/demux/transmuxer.ts +16 -3
- package/src/demux/tsdemuxer.ts +71 -37
- package/src/demux/video/avc-video-parser.ts +208 -119
- package/src/demux/video/base-video-parser.ts +134 -2
- package/src/demux/video/exp-golomb.ts +0 -208
- package/src/demux/video/hevc-video-parser.ts +746 -0
- package/src/events.ts +7 -0
- package/src/hls.ts +49 -37
- package/src/loader/fragment-loader.ts +9 -2
- package/src/loader/key-loader.ts +2 -0
- package/src/loader/level-key.ts +10 -9
- package/src/loader/playlist-loader.ts +4 -5
- package/src/remux/mp4-generator.ts +196 -1
- package/src/remux/mp4-remuxer.ts +23 -7
- package/src/task-loop.ts +5 -2
- package/src/types/component-api.ts +2 -0
- package/src/types/demuxer.ts +3 -0
- package/src/types/events.ts +4 -0
- package/src/utils/buffer-helper.ts +12 -31
- package/src/utils/codecs.ts +34 -5
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/logger.ts +54 -24
- package/src/utils/mp4-tools.ts +4 -2
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Events } from '../events';
|
2
|
-
import {
|
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 {
|
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
|
-
|
86
|
-
|
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
|
-
|
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
|
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 {
|
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
|
-
|
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: ${
|
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
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
execute:
|
555
|
-
this,
|
556
|
-
|
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
|
-
|
573
|
-
|
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((
|
576
|
-
operationQueue.append(
|
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.
|
785
|
+
this.blockBuffers(() => this.updateMediaSource(durationAndRange));
|
677
786
|
} else {
|
678
|
-
this.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
45
|
-
execute,
|
46
|
-
onStart: () => {},
|
47
|
-
onComplete: () => {},
|
48
|
-
onError: () => {},
|
49
|
-
};
|
43
|
+
}
|
50
44
|
|
51
|
-
|
52
|
-
|
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,
|
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 {
|
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
|
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
|
-
|
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
|
447
|
+
if (steeringData?.VERSION !== 1) {
|
446
448
|
this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
|
447
449
|
return;
|
448
450
|
}
|