hls.js 1.6.0-beta.2.0.canary.10924 → 1.6.0-beta.2.0.canary.10926

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.
@@ -1,7 +1,7 @@
1
1
  import BaseStreamController, { State } from './base-stream-controller';
2
2
  import { findFragmentByPTS } from './fragment-finders';
3
3
  import { FragmentState } from './fragment-tracker';
4
- import GapController, { MAX_START_GAP_JUMP } from './gap-controller';
4
+ import { MAX_START_GAP_JUMP } from './gap-controller';
5
5
  import TransmuxerInterface from '../demux/transmuxer-interface';
6
6
  import { ErrorDetails } from '../errors';
7
7
  import { Events } from '../events';
@@ -11,13 +11,21 @@ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
11
11
  import { ChunkMetadata } from '../types/transmuxer';
12
12
  import { BufferHelper } from '../utils/buffer-helper';
13
13
  import { pickMostCompleteCodecName } from '../utils/codecs';
14
+ import {
15
+ addEventListener,
16
+ removeEventListener,
17
+ } from '../utils/event-listener-helper';
14
18
  import { useAlternateAudio } from '../utils/rendition-helper';
15
19
  import type { FragmentTracker } from './fragment-tracker';
16
20
  import type Hls from '../hls';
17
21
  import type { Fragment, MediaFragment } from '../loader/fragment';
18
22
  import type KeyLoader from '../loader/key-loader';
19
23
  import type { LevelDetails } from '../loader/level-details';
20
- import type { SourceBufferName } from '../types/buffer';
24
+ import type {
25
+ BufferCreatedTrack,
26
+ ExtendedSourceBuffer,
27
+ SourceBufferName,
28
+ } from '../types/buffer';
21
29
  import type { NetworkComponentAPI } from '../types/component-api';
22
30
  import type {
23
31
  AudioTrackSwitchedData,
@@ -56,7 +64,6 @@ export default class StreamController
56
64
  implements NetworkComponentAPI
57
65
  {
58
66
  private audioCodecSwap: boolean = false;
59
- private gapController: GapController | null = null;
60
67
  private level: number = -1;
61
68
  private _forceStartLoad: boolean = false;
62
69
  private _hasEnoughToStart: boolean = false;
@@ -67,7 +74,7 @@ export default class StreamController
67
74
  private couldBacktrack: boolean = false;
68
75
  private backtrackFragment: Fragment | null = null;
69
76
  private audioCodecSwitch: boolean = false;
70
- private videoBuffer: any | null = null;
77
+ private videoBuffer: ExtendedSourceBuffer | null = null;
71
78
 
72
79
  constructor(
73
80
  hls: Hls,
@@ -123,7 +130,7 @@ export default class StreamController
123
130
 
124
131
  protected onHandlerDestroying() {
125
132
  // @ts-ignore
126
- this.onMediaPlaying = this.onMediaSeeked = this.onMediaWaiting = null;
133
+ this.onMediaPlaying = this.onMediaSeeked = null;
127
134
  this.unregisterListeners();
128
135
  super.onHandlerDestroying();
129
136
  }
@@ -230,7 +237,9 @@ export default class StreamController
230
237
 
231
238
  protected onTickEnd() {
232
239
  super.onTickEnd();
233
- this.checkBuffer();
240
+ if (this.media?.readyState && this.media.seeking === false) {
241
+ this.lastCurrentTime = this.media.currentTime;
242
+ }
234
243
  this.checkFragmentChanged();
235
244
  }
236
245
 
@@ -533,17 +542,8 @@ export default class StreamController
533
542
  ) {
534
543
  super.onMediaAttached(event, data);
535
544
  const media = data.media;
536
- media.removeEventListener('playing', this.onMediaPlaying);
537
- media.removeEventListener('seeked', this.onMediaSeeked);
538
- media.removeEventListener('waiting', this.onMediaWaiting);
539
- media.addEventListener('playing', this.onMediaPlaying);
540
- media.addEventListener('seeked', this.onMediaSeeked);
541
- media.addEventListener('waiting', this.onMediaWaiting);
542
- this.gapController = new GapController(
543
- media,
544
- this.fragmentTracker,
545
- this.hls,
546
- );
545
+ addEventListener(media, 'playing', this.onMediaPlaying);
546
+ addEventListener(media, 'seeked', this.onMediaSeeked);
547
547
  }
548
548
 
549
549
  protected onMediaDetaching(
@@ -552,16 +552,11 @@ export default class StreamController
552
552
  ) {
553
553
  const { media } = this;
554
554
  if (media) {
555
- media.removeEventListener('playing', this.onMediaPlaying);
556
- media.removeEventListener('seeked', this.onMediaSeeked);
557
- media.removeEventListener('waiting', this.onMediaWaiting);
555
+ removeEventListener(media, 'playing', this.onMediaPlaying);
556
+ removeEventListener(media, 'seeked', this.onMediaSeeked);
558
557
  }
559
558
  this.videoBuffer = null;
560
559
  this.fragPlaying = null;
561
- if (this.gapController) {
562
- this.gapController.destroy();
563
- this.gapController = null;
564
- }
565
560
  super.onMediaDetaching(event, data);
566
561
  const transferringMedia = !!data.transferMedia;
567
562
  if (transferringMedia) {
@@ -570,20 +565,8 @@ export default class StreamController
570
565
  this._hasEnoughToStart = false;
571
566
  }
572
567
 
573
- private onMediaWaiting = () => {
574
- const gapController = this.gapController;
575
- if (gapController) {
576
- gapController.waiting = self.performance.now();
577
- }
578
- };
579
-
580
568
  private onMediaPlaying = () => {
581
569
  // tick to speed up FRAG_CHANGED triggering
582
- const gapController = this.gapController;
583
- if (gapController) {
584
- gapController.ended = 0;
585
- gapController.waiting = 0;
586
- }
587
570
  this.tick();
588
571
  };
589
572
 
@@ -609,19 +592,6 @@ export default class StreamController
609
592
  this.tick();
610
593
  };
611
594
 
612
- protected triggerEnded() {
613
- const gapController = this.gapController;
614
- if (gapController) {
615
- if (gapController.ended) {
616
- return;
617
- }
618
- gapController.ended = this.media?.currentTime || 1;
619
- }
620
- this.hls.trigger(Events.MEDIA_ENDED, {
621
- stalled: false,
622
- });
623
- }
624
-
625
595
  protected onManifestLoading() {
626
596
  super.onManifestLoading();
627
597
  // reset buffer on manifest loading
@@ -935,11 +905,11 @@ export default class StreamController
935
905
  data: BufferCreatedData,
936
906
  ) {
937
907
  const tracks = data.tracks;
938
- let mediaTrack;
939
- let name;
908
+ let mediaTrack: BufferCreatedTrack | undefined;
909
+ let name: string | undefined;
940
910
  let alternate = false;
941
911
  for (const type in tracks) {
942
- const track = tracks[type];
912
+ const track: BufferCreatedTrack = tracks[type];
943
913
  if (track.id === 'main') {
944
914
  name = type;
945
915
  mediaTrack = track;
@@ -1056,25 +1026,6 @@ export default class StreamController
1056
1026
  }
1057
1027
  }
1058
1028
 
1059
- // Checks the health of the buffer and attempts to resolve playback stalls.
1060
- private checkBuffer() {
1061
- const { media, gapController } = this;
1062
- if (!media || !gapController || !media.readyState) {
1063
- // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
1064
- return;
1065
- }
1066
-
1067
- if (this._hasEnoughToStart || !BufferHelper.getBuffered(media).length) {
1068
- // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
1069
- const state = this.state;
1070
- const activeFrag = state !== State.IDLE ? this.fragCurrent : null;
1071
- const levelDetails = this.getLevelDetails();
1072
- gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state);
1073
- }
1074
-
1075
- this.lastCurrentTime = media.currentTime;
1076
- }
1077
-
1078
1029
  private onFragLoadEmergencyAborted() {
1079
1030
  this.state = State.IDLE;
1080
1031
  // if loadedmetadata is not set, it means that we are emergency switch down on first frag
@@ -1095,8 +1046,10 @@ export default class StreamController
1095
1046
  (type === ElementaryStreamTypes.VIDEO
1096
1047
  ? this.videoBuffer
1097
1048
  : this.mediaBuffer) || this.media;
1098
- this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
1099
- this.tick();
1049
+ if (mediaBuffer) {
1050
+ this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
1051
+ this.tick();
1052
+ }
1100
1053
  }
1101
1054
  }
1102
1055
 
package/src/hls.ts CHANGED
@@ -3,6 +3,7 @@ import { EventEmitter } from 'eventemitter3';
3
3
  import { buildAbsoluteURL } from 'url-toolkit';
4
4
  import { enableStreamingMode, hlsDefaultConfig, mergeConfig } from './config';
5
5
  import { FragmentTracker } from './controller/fragment-tracker';
6
+ import GapController from './controller/gap-controller';
6
7
  import ID3TrackController from './controller/id3-track-controller';
7
8
  import LatencyController from './controller/latency-controller';
8
9
  import LevelController from './controller/level-controller';
@@ -14,6 +15,7 @@ import KeyLoader from './loader/key-loader';
14
15
  import PlaylistLoader from './loader/playlist-loader';
15
16
  import { MetadataSchema } from './types/demuxer';
16
17
  import { type HdcpLevel, isHdcpLevel, type Level } from './types/level';
18
+ import { PlaylistLevelType } from './types/loader';
17
19
  import { enableLogs, type ILogger } from './utils/logger';
18
20
  import { getMediaDecodingInfoPromise } from './utils/mediacapabilities-helper';
19
21
  import { getMediaSource } from './utils/mediasource-helper';
@@ -24,6 +26,7 @@ import type AbrController from './controller/abr-controller';
24
26
  import type AudioStreamController from './controller/audio-stream-controller';
25
27
  import type AudioTrackController from './controller/audio-track-controller';
26
28
  import type BasePlaylistController from './controller/base-playlist-controller';
29
+ import type { InFlightData, State } from './controller/base-stream-controller';
27
30
  import type BaseStreamController from './controller/base-stream-controller';
28
31
  import type BufferController from './controller/buffer-controller';
29
32
  import type CapLevelController from './controller/cap-level-controller';
@@ -34,6 +37,7 @@ import type ErrorController from './controller/error-controller';
34
37
  import type FPSController from './controller/fps-controller';
35
38
  import type InterstitialsController from './controller/interstitials-controller';
36
39
  import type { InterstitialsManager } from './controller/interstitials-controller';
40
+ import type { SubtitleStreamController } from './controller/subtitle-stream-controller';
37
41
  import type SubtitleTrackController from './controller/subtitle-track-controller';
38
42
  import type Decrypter from './crypt/decrypter';
39
43
  import type TransmuxerInterface from './demux/transmuxer-interface';
@@ -91,9 +95,12 @@ export default class Hls implements HlsEventEmitter {
91
95
  private latencyController: LatencyController;
92
96
  private levelController: LevelController;
93
97
  private streamController: StreamController;
98
+ private audioStreamController?: AudioStreamController;
99
+ private subtititleStreamController?: SubtitleStreamController;
94
100
  private audioTrackController?: AudioTrackController;
95
101
  private subtitleTrackController?: SubtitleTrackController;
96
102
  private interstitialsController?: InterstitialsController;
103
+ private gapController: GapController;
97
104
  private emeController?: EMEController;
98
105
  private cmcdController?: CMCDController;
99
106
  private _media: HTMLMediaElement | null = null;
@@ -229,6 +236,11 @@ export default class Hls implements HlsEventEmitter {
229
236
  keyLoader,
230
237
  ));
231
238
 
239
+ const gapController = (this.gapController = new GapController(
240
+ this,
241
+ fragmentTracker,
242
+ ));
243
+
232
244
  // Cap level controller uses streamController to flush the buffer
233
245
  capLevelController.setStreamController(streamController);
234
246
  // fpsController uses streamController to switch when frames are being dropped
@@ -250,6 +262,7 @@ export default class Hls implements HlsEventEmitter {
250
262
  const coreComponents: ComponentAPI[] = [
251
263
  abrController,
252
264
  bufferController,
265
+ gapController,
253
266
  capLevelController,
254
267
  fpsController,
255
268
  id3TrackController,
@@ -263,7 +276,11 @@ export default class Hls implements HlsEventEmitter {
263
276
  const AudioStreamControllerClass = config.audioStreamController;
264
277
  if (AudioStreamControllerClass) {
265
278
  networkControllers.push(
266
- new AudioStreamControllerClass(this, fragmentTracker, keyLoader),
279
+ (this.audioStreamController = new AudioStreamControllerClass(
280
+ this,
281
+ fragmentTracker,
282
+ keyLoader,
283
+ )),
267
284
  );
268
285
  }
269
286
  // Instantiate subtitleTrackController before SubtitleStreamController to receive level events first
@@ -274,7 +291,11 @@ export default class Hls implements HlsEventEmitter {
274
291
  const SubtitleStreamControllerClass = config.subtitleStreamController;
275
292
  if (SubtitleStreamControllerClass) {
276
293
  networkControllers.push(
277
- new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader),
294
+ (this.subtititleStreamController = new SubtitleStreamControllerClass(
295
+ this,
296
+ fragmentTracker,
297
+ keyLoader,
298
+ )),
278
299
  );
279
300
  }
280
301
  this.createController(config.timelineController, coreComponents);
@@ -604,6 +625,21 @@ export default class Hls implements HlsEventEmitter {
604
625
  }
605
626
  }
606
627
 
628
+ get inFlightFragments(): InFlightFragments {
629
+ const inFlightData = {
630
+ [PlaylistLevelType.MAIN]: this.streamController.inFlightFrag,
631
+ };
632
+ if (this.audioStreamController) {
633
+ inFlightData[PlaylistLevelType.AUDIO] =
634
+ this.audioStreamController.inFlightFrag;
635
+ }
636
+ if (this.subtititleStreamController) {
637
+ inFlightData[PlaylistLevelType.SUBTITLE] =
638
+ this.subtititleStreamController.inFlightFrag;
639
+ }
640
+ return inFlightData;
641
+ }
642
+
607
643
  /**
608
644
  * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)
609
645
  */
@@ -654,10 +690,20 @@ export default class Hls implements HlsEventEmitter {
654
690
  return levels ? levels : [];
655
691
  }
656
692
 
693
+ /**
694
+ * @returns LevelDetails of last loaded level (variant) or `null` prior to loading a media playlist.
695
+ */
657
696
  get latestLevelDetails(): LevelDetails | null {
658
697
  return this.streamController.getLevelDetails() || null;
659
698
  }
660
699
 
700
+ /**
701
+ * @returns Level object of selected level (variant) or `null` prior to selecting a level or once the level is removed.
702
+ */
703
+ get loadLevelObj(): Level | null {
704
+ return this.levelController.loadLevelObj;
705
+ }
706
+
661
707
  /**
662
708
  * Index of quality level (variant) currently played
663
709
  */
@@ -1181,6 +1227,11 @@ export default class Hls implements HlsEventEmitter {
1181
1227
  }
1182
1228
  }
1183
1229
 
1230
+ export type InFlightFragments = {
1231
+ [PlaylistLevelType.MAIN]: InFlightData;
1232
+ [PlaylistLevelType.AUDIO]?: InFlightData;
1233
+ [PlaylistLevelType.SUBTITLE]?: InFlightData;
1234
+ };
1184
1235
  export type {
1185
1236
  AudioSelectionOption,
1186
1237
  SubtitleSelectionOption,
@@ -1211,6 +1262,7 @@ export type {
1211
1262
  FPSController,
1212
1263
  InterstitialsController,
1213
1264
  StreamController,
1265
+ SubtitleStreamController,
1214
1266
  SubtitleTrackController,
1215
1267
  EwmaBandWidthEstimator,
1216
1268
  InterstitialsManager,
@@ -1219,6 +1271,8 @@ export type {
1219
1271
  KeyLoader,
1220
1272
  TaskLoop,
1221
1273
  TransmuxerInterface,
1274
+ InFlightData,
1275
+ State,
1222
1276
  };
1223
1277
  export type {
1224
1278
  ABRControllerConfig,
@@ -1232,6 +1286,7 @@ export type {
1232
1286
  FPSControllerConfig,
1233
1287
  FragmentLoaderConfig,
1234
1288
  FragmentLoaderConstructor,
1289
+ GapControllerConfig,
1235
1290
  HlsLoadPolicies,
1236
1291
  LevelControllerConfig,
1237
1292
  LoaderConfig,
@@ -1270,7 +1325,6 @@ export type {
1270
1325
  InterstitialScheduleItem,
1271
1326
  InterstitialSchedulePrimaryItem,
1272
1327
  } from './controller/interstitials-schedule';
1273
- export type { SubtitleStreamController } from './controller/subtitle-stream-controller';
1274
1328
  export type { TimelineController } from './controller/timeline-controller';
1275
1329
  export type { DecrypterAesMode } from './crypt/decrypter-aes-mode';
1276
1330
  export type { DateRange, DateRangeCue } from './loader/date-range';
@@ -23,6 +23,7 @@ export type BufferInfo = {
23
23
  end: number;
24
24
  nextStart?: number;
25
25
  buffered?: BufferTimeRange[];
26
+ bufferedIndex: number;
26
27
  };
27
28
 
28
29
  const noopBuffered: TimeRanges = {
@@ -47,22 +48,34 @@ export class BufferHelper {
47
48
  return false;
48
49
  }
49
50
 
51
+ static bufferedRanges(media: Bufferable | null): BufferTimeRange[] {
52
+ if (media) {
53
+ const timeRanges = BufferHelper.getBuffered(media);
54
+ return BufferHelper.timeRangesToArray(timeRanges);
55
+ }
56
+ return [];
57
+ }
58
+
59
+ static timeRangesToArray(timeRanges: TimeRanges): BufferTimeRange[] {
60
+ const buffered: BufferTimeRange[] = [];
61
+ for (let i = 0; i < timeRanges.length; i++) {
62
+ buffered.push({ start: timeRanges.start(i), end: timeRanges.end(i) });
63
+ }
64
+ return buffered;
65
+ }
66
+
50
67
  static bufferInfo(
51
68
  media: Bufferable | null,
52
69
  pos: number,
53
70
  maxHoleDuration: number,
54
71
  ): BufferInfo {
55
72
  if (media) {
56
- const vbuffered = BufferHelper.getBuffered(media);
57
- if (vbuffered.length) {
58
- const buffered: BufferTimeRange[] = [];
59
- for (let i = 0; i < vbuffered.length; i++) {
60
- buffered.push({ start: vbuffered.start(i), end: vbuffered.end(i) });
61
- }
73
+ const buffered = BufferHelper.bufferedRanges(media);
74
+ if (buffered.length) {
62
75
  return BufferHelper.bufferedInfo(buffered, pos, maxHoleDuration);
63
76
  }
64
77
  }
65
- return { len: 0, start: pos, end: pos };
78
+ return { len: 0, start: pos, end: pos, bufferedIndex: -1 };
66
79
  }
67
80
 
68
81
  static bufferedInfo(
@@ -72,14 +85,20 @@ export class BufferHelper {
72
85
  ): BufferInfo {
73
86
  pos = Math.max(0, pos);
74
87
  // sort on buffer.start/smaller end (IE does not always return sorted buffered range)
75
- buffered.sort((a, b) => a.start - b.start || b.end - a.end);
88
+ if (buffered.length > 1) {
89
+ buffered.sort((a, b) => a.start - b.start || b.end - a.end);
90
+ }
76
91
 
92
+ let bufferedIndex: number = -1;
77
93
  let buffered2: BufferTimeRange[] = [];
78
94
  if (maxHoleDuration) {
79
95
  // there might be some small holes between buffer time range
80
96
  // consider that holes smaller than maxHoleDuration are irrelevant and build another
81
97
  // buffer time range representations that discards those holes
82
98
  for (let i = 0; i < buffered.length; i++) {
99
+ if (pos >= buffered[i].start && pos <= buffered[i].end) {
100
+ bufferedIndex = i;
101
+ }
83
102
  const buf2len = buffered2.length;
84
103
  if (buf2len) {
85
104
  const buf2end = buffered2[buf2len - 1].end;
@@ -107,23 +126,25 @@ export class BufferHelper {
107
126
 
108
127
  let bufferLen = 0;
109
128
 
110
- // bufferStartNext can possibly be undefined based on the conditional logic below
111
- let bufferStartNext: number | undefined;
129
+ let nextStart: number | undefined;
112
130
 
113
- // bufferStart and bufferEnd are buffer boundaries around current video position
131
+ // bufferStart and bufferEnd are buffer boundaries around current playback position (pos)
114
132
  let bufferStart: number = pos;
115
133
  let bufferEnd: number = pos;
116
134
  for (let i = 0; i < buffered2.length; i++) {
117
135
  const start = buffered2[i].start;
118
136
  const end = buffered2[i].end;
119
137
  // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
138
+ if (bufferedIndex === -1 && pos >= start && pos <= end) {
139
+ bufferedIndex = i;
140
+ }
120
141
  if (pos + maxHoleDuration >= start && pos < end) {
121
142
  // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
122
143
  bufferStart = start;
123
144
  bufferEnd = end;
124
145
  bufferLen = bufferEnd - pos;
125
146
  } else if (pos + maxHoleDuration < start) {
126
- bufferStartNext = start;
147
+ nextStart = start;
127
148
  break;
128
149
  }
129
150
  }
@@ -131,8 +152,9 @@ export class BufferHelper {
131
152
  len: bufferLen,
132
153
  start: bufferStart || 0,
133
154
  end: bufferEnd || 0,
134
- nextStart: bufferStartNext,
155
+ nextStart,
135
156
  buffered,
157
+ bufferedIndex,
136
158
  };
137
159
  }
138
160
 
@@ -0,0 +1,16 @@
1
+ export function addEventListener(
2
+ el: HTMLElement,
3
+ type: string,
4
+ listener: EventListenerOrEventListenerObject,
5
+ ) {
6
+ removeEventListener(el, type, listener);
7
+ el.addEventListener(type, listener);
8
+ }
9
+
10
+ export function removeEventListener(
11
+ el: HTMLElement,
12
+ type: string,
13
+ listener: EventListenerOrEventListenerObject,
14
+ ) {
15
+ el.removeEventListener(type, listener);
16
+ }
@@ -502,5 +502,5 @@ export function useAlternateAudio(
502
502
  audioTrackUrl: string | undefined,
503
503
  hls: Hls,
504
504
  ): boolean {
505
- return !!audioTrackUrl && audioTrackUrl !== hls.levels[hls.loadLevel]?.uri;
505
+ return !!audioTrackUrl && audioTrackUrl !== hls.loadLevelObj?.uri;
506
506
  }