hls.js 1.6.0-beta.2.0.canary.10923 → 1.6.0-beta.2.0.canary.10925

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.
@@ -22,6 +22,10 @@ import {
22
22
  TimelineOccupancy,
23
23
  } from '../loader/interstitial-event';
24
24
  import { BufferHelper } from '../utils/buffer-helper';
25
+ import {
26
+ addEventListener,
27
+ removeEventListener,
28
+ } from '../utils/event-listener-helper';
25
29
  import { hash } from '../utils/hash';
26
30
  import { Logger } from '../utils/logger';
27
31
  import { isCompatibleTrackChange } from '../utils/mediasource-helper';
@@ -226,17 +230,17 @@ export default class InterstitialsController
226
230
  }
227
231
 
228
232
  private onDestroying() {
229
- const media = this.primaryMedia;
233
+ const media = this.primaryMedia || this.media;
230
234
  if (media) {
231
235
  this.removeMediaListeners(media);
232
236
  }
233
237
  }
234
238
 
235
239
  private removeMediaListeners(media: HTMLMediaElement) {
236
- media.removeEventListener('play', this.onPlay);
237
- media.removeEventListener('pause', this.onPause);
238
- media.removeEventListener('seeking', this.onSeeking);
239
- media.removeEventListener('timeupdate', this.onTimeupdate);
240
+ removeEventListener(media, 'play', this.onPlay);
241
+ removeEventListener(media, 'pause', this.onPause);
242
+ removeEventListener(media, 'seeking', this.onSeeking);
243
+ removeEventListener(media, 'timeupdate', this.onTimeupdate);
240
244
  }
241
245
 
242
246
  private onMediaAttaching(
@@ -244,11 +248,10 @@ export default class InterstitialsController
244
248
  data: MediaAttachingData,
245
249
  ) {
246
250
  const media = (this.media = data.media);
247
- this.removeMediaListeners(media);
248
- media.addEventListener('seeking', this.onSeeking);
249
- media.addEventListener('timeupdate', this.onTimeupdate);
250
- media.addEventListener('play', this.onPlay);
251
- media.addEventListener('pause', this.onPause);
251
+ addEventListener(media, 'seeking', this.onSeeking);
252
+ addEventListener(media, 'timeupdate', this.onTimeupdate);
253
+ addEventListener(media, 'play', this.onPlay);
254
+ addEventListener(media, 'pause', this.onPause);
252
255
  }
253
256
 
254
257
  private onMediaAttached(
@@ -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) {
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
  */
@@ -1181,6 +1217,11 @@ export default class Hls implements HlsEventEmitter {
1181
1217
  }
1182
1218
  }
1183
1219
 
1220
+ export type InFlightFragments = {
1221
+ [PlaylistLevelType.MAIN]: InFlightData;
1222
+ [PlaylistLevelType.AUDIO]?: InFlightData;
1223
+ [PlaylistLevelType.SUBTITLE]?: InFlightData;
1224
+ };
1184
1225
  export type {
1185
1226
  AudioSelectionOption,
1186
1227
  SubtitleSelectionOption,
@@ -1211,6 +1252,7 @@ export type {
1211
1252
  FPSController,
1212
1253
  InterstitialsController,
1213
1254
  StreamController,
1255
+ SubtitleStreamController,
1214
1256
  SubtitleTrackController,
1215
1257
  EwmaBandWidthEstimator,
1216
1258
  InterstitialsManager,
@@ -1219,6 +1261,8 @@ export type {
1219
1261
  KeyLoader,
1220
1262
  TaskLoop,
1221
1263
  TransmuxerInterface,
1264
+ InFlightData,
1265
+ State,
1222
1266
  };
1223
1267
  export type {
1224
1268
  ABRControllerConfig,
@@ -1232,6 +1276,7 @@ export type {
1232
1276
  FPSControllerConfig,
1233
1277
  FragmentLoaderConfig,
1234
1278
  FragmentLoaderConstructor,
1279
+ GapControllerConfig,
1235
1280
  HlsLoadPolicies,
1236
1281
  LevelControllerConfig,
1237
1282
  LoaderConfig,
@@ -1270,7 +1315,6 @@ export type {
1270
1315
  InterstitialScheduleItem,
1271
1316
  InterstitialSchedulePrimaryItem,
1272
1317
  } from './controller/interstitials-schedule';
1273
- export type { SubtitleStreamController } from './controller/subtitle-stream-controller';
1274
1318
  export type { TimelineController } from './controller/timeline-controller';
1275
1319
  export type { DecrypterAesMode } from './crypt/decrypter-aes-mode';
1276
1320
  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
+ }