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