hls.js 1.5.13 → 1.5.14-0.canary.10417

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +4 -3
  2. package/dist/hls-demo.js +41 -38
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +4211 -2666
  5. package/dist/hls.js.d.ts +179 -110
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2841 -1921
  8. package/dist/hls.light.js.map +1 -1
  9. package/dist/hls.light.min.js +1 -1
  10. package/dist/hls.light.min.js.map +1 -1
  11. package/dist/hls.light.mjs +2569 -1639
  12. package/dist/hls.light.mjs.map +1 -1
  13. package/dist/hls.min.js +1 -1
  14. package/dist/hls.min.js.map +1 -1
  15. package/dist/hls.mjs +3572 -2017
  16. package/dist/hls.mjs.map +1 -1
  17. package/dist/hls.worker.js +1 -1
  18. package/dist/hls.worker.js.map +1 -1
  19. package/package.json +38 -38
  20. package/src/config.ts +5 -2
  21. package/src/controller/abr-controller.ts +39 -25
  22. package/src/controller/audio-stream-controller.ts +156 -136
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +27 -10
  25. package/src/controller/base-stream-controller.ts +234 -89
  26. package/src/controller/buffer-controller.ts +250 -97
  27. package/src/controller/buffer-operation-queue.ts +16 -19
  28. package/src/controller/cap-level-controller.ts +3 -2
  29. package/src/controller/cmcd-controller.ts +51 -14
  30. package/src/controller/content-steering-controller.ts +29 -15
  31. package/src/controller/eme-controller.ts +10 -23
  32. package/src/controller/error-controller.ts +28 -22
  33. package/src/controller/fps-controller.ts +8 -3
  34. package/src/controller/fragment-finders.ts +44 -16
  35. package/src/controller/fragment-tracker.ts +58 -25
  36. package/src/controller/gap-controller.ts +43 -16
  37. package/src/controller/id3-track-controller.ts +45 -35
  38. package/src/controller/latency-controller.ts +18 -13
  39. package/src/controller/level-controller.ts +37 -19
  40. package/src/controller/stream-controller.ts +100 -83
  41. package/src/controller/subtitle-stream-controller.ts +35 -47
  42. package/src/controller/subtitle-track-controller.ts +5 -3
  43. package/src/controller/timeline-controller.ts +20 -22
  44. package/src/crypt/aes-crypto.ts +21 -2
  45. package/src/crypt/decrypter-aes-mode.ts +4 -0
  46. package/src/crypt/decrypter.ts +32 -16
  47. package/src/crypt/fast-aes-key.ts +28 -5
  48. package/src/demux/audio/aacdemuxer.ts +2 -2
  49. package/src/demux/audio/ac3-demuxer.ts +4 -3
  50. package/src/demux/audio/adts.ts +9 -4
  51. package/src/demux/audio/base-audio-demuxer.ts +16 -14
  52. package/src/demux/audio/mp3demuxer.ts +4 -3
  53. package/src/demux/audio/mpegaudio.ts +1 -1
  54. package/src/demux/mp4demuxer.ts +7 -7
  55. package/src/demux/sample-aes.ts +2 -0
  56. package/src/demux/transmuxer-interface.ts +8 -16
  57. package/src/demux/transmuxer-worker.ts +4 -4
  58. package/src/demux/transmuxer.ts +16 -3
  59. package/src/demux/tsdemuxer.ts +75 -38
  60. package/src/demux/video/avc-video-parser.ts +210 -121
  61. package/src/demux/video/base-video-parser.ts +135 -2
  62. package/src/demux/video/exp-golomb.ts +0 -208
  63. package/src/demux/video/hevc-video-parser.ts +749 -0
  64. package/src/events.ts +8 -1
  65. package/src/exports-named.ts +1 -1
  66. package/src/hls.ts +84 -47
  67. package/src/loader/date-range.ts +71 -5
  68. package/src/loader/fragment-loader.ts +23 -21
  69. package/src/loader/fragment.ts +8 -4
  70. package/src/loader/key-loader.ts +3 -1
  71. package/src/loader/level-details.ts +6 -6
  72. package/src/loader/level-key.ts +10 -9
  73. package/src/loader/m3u8-parser.ts +138 -144
  74. package/src/loader/playlist-loader.ts +5 -7
  75. package/src/remux/mp4-generator.ts +196 -1
  76. package/src/remux/mp4-remuxer.ts +32 -62
  77. package/src/remux/passthrough-remuxer.ts +1 -1
  78. package/src/task-loop.ts +5 -2
  79. package/src/types/component-api.ts +3 -1
  80. package/src/types/demuxer.ts +3 -0
  81. package/src/types/events.ts +19 -6
  82. package/src/types/fragment-tracker.ts +2 -2
  83. package/src/types/media-playlist.ts +9 -1
  84. package/src/types/remuxer.ts +1 -1
  85. package/src/utils/attr-list.ts +96 -9
  86. package/src/utils/buffer-helper.ts +12 -31
  87. package/src/utils/cea-608-parser.ts +1 -3
  88. package/src/utils/codecs.ts +34 -5
  89. package/src/utils/encryption-methods-util.ts +21 -0
  90. package/src/utils/fetch-loader.ts +1 -1
  91. package/src/utils/hash.ts +10 -0
  92. package/src/utils/hdr.ts +4 -7
  93. package/src/utils/imsc1-ttml-parser.ts +1 -1
  94. package/src/utils/keysystem-util.ts +1 -6
  95. package/src/utils/level-helper.ts +71 -44
  96. package/src/utils/logger.ts +58 -23
  97. package/src/utils/mp4-tools.ts +5 -3
  98. package/src/utils/rendition-helper.ts +100 -74
  99. package/src/utils/utf8-utils.ts +18 -0
  100. package/src/utils/variable-substitution.ts +0 -19
  101. package/src/utils/webvtt-parser.ts +2 -12
  102. package/src/demux/id3.ts +0 -411
  103. package/src/types/general.ts +0 -6
@@ -1,14 +1,18 @@
1
1
  import BaseStreamController, { State } from './base-stream-controller';
2
2
  import { Events } from '../events';
3
- import { Bufferable, BufferHelper } from '../utils/buffer-helper';
4
3
  import { FragmentState } from './fragment-tracker';
5
4
  import { Level } from '../types/level';
6
5
  import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
7
- import { Fragment, ElementaryStreamTypes, Part } from '../loader/fragment';
6
+ import {
7
+ Fragment,
8
+ ElementaryStreamTypes,
9
+ Part,
10
+ MediaFragment,
11
+ } from '../loader/fragment';
8
12
  import ChunkCache from '../demux/chunk-cache';
9
13
  import TransmuxerInterface from '../demux/transmuxer-interface';
10
14
  import { ChunkMetadata } from '../types/transmuxer';
11
- import { fragmentWithinToleranceTest } from './fragment-finders';
15
+ import { findFragWithCC, findNearestWithCC } from './fragment-finders';
12
16
  import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
13
17
  import { mediaAttributesIdentical } from '../utils/media-option-attributes';
14
18
  import { ErrorDetails } from '../errors';
@@ -34,13 +38,15 @@ import type {
34
38
  FragBufferedData,
35
39
  ErrorData,
36
40
  BufferFlushingData,
41
+ BufferCodecsData,
42
+ FragLoadingData,
37
43
  } from '../types/events';
38
44
  import type { MediaPlaylist } from '../types/media-playlist';
39
45
 
40
46
  const TICK_INTERVAL = 100; // how often to tick in ms
41
47
 
42
48
  type WaitingForPTSData = {
43
- frag: Fragment;
49
+ frag: MediaFragment;
44
50
  part: Part | null;
45
51
  cache: ChunkCache;
46
52
  complete: boolean;
@@ -50,9 +56,8 @@ class AudioStreamController
50
56
  extends BaseStreamController
51
57
  implements NetworkComponentAPI
52
58
  {
53
- private videoBuffer: Bufferable | null = null;
54
- private videoTrackCC: number = -1;
55
- private waitingVideoCC: number = -1;
59
+ private videoAnchor: MediaFragment | null = null;
60
+ private mainFragLoading: FragLoadingData | null = null;
56
61
  private bufferedTrack: MediaPlaylist | null = null;
57
62
  private switchingTrack: MediaPlaylist | null = null;
58
63
  private trackId: number = -1;
@@ -71,53 +76,52 @@ class AudioStreamController
71
76
  hls,
72
77
  fragmentTracker,
73
78
  keyLoader,
74
- '[audio-stream-controller]',
79
+ 'audio-stream-controller',
75
80
  PlaylistLevelType.AUDIO,
76
81
  );
77
- this._registerListeners();
82
+ this.registerListeners();
78
83
  }
79
84
 
80
85
  protected onHandlerDestroying() {
81
- this._unregisterListeners();
86
+ this.unregisterListeners();
82
87
  super.onHandlerDestroying();
83
88
  this.mainDetails = null;
84
89
  this.bufferedTrack = null;
85
90
  this.switchingTrack = null;
86
91
  }
87
92
 
88
- private _registerListeners() {
93
+ protected registerListeners() {
94
+ super.registerListeners();
89
95
  const { hls } = this;
90
- hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
91
- hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
92
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
93
96
  hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
94
97
  hls.on(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this);
95
98
  hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
96
99
  hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
97
- hls.on(Events.ERROR, this.onError, this);
98
100
  hls.on(Events.BUFFER_RESET, this.onBufferReset, this);
99
101
  hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this);
100
102
  hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
101
103
  hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
102
104
  hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
105
+ hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
103
106
  hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
104
107
  }
105
108
 
106
- private _unregisterListeners() {
109
+ protected unregisterListeners() {
107
110
  const { hls } = this;
108
- hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
109
- hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
110
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
111
+ if (!hls) {
112
+ return;
113
+ }
114
+ super.unregisterListeners();
111
115
  hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
112
116
  hls.off(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this);
113
117
  hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
114
118
  hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
115
- hls.off(Events.ERROR, this.onError, this);
116
119
  hls.off(Events.BUFFER_RESET, this.onBufferReset, this);
117
120
  hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this);
118
121
  hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
119
122
  hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
120
123
  hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
124
+ hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
121
125
  hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
122
126
  }
123
127
 
@@ -128,18 +132,44 @@ class AudioStreamController
128
132
  ) {
129
133
  // Always update the new INIT PTS
130
134
  // Can change due level switch
131
- if (id === 'main') {
135
+ if (id === PlaylistLevelType.MAIN) {
132
136
  const cc = frag.cc;
133
- this.initPTS[frag.cc] = { baseTime: initPTS, timescale };
134
- this.log(`InitPTS for cc: ${cc} found from main: ${initPTS}`);
135
- this.videoTrackCC = cc;
137
+ const inFlightFrag = this.fragCurrent;
138
+ this.initPTS[cc] = { baseTime: initPTS, timescale };
139
+ this.log(
140
+ `InitPTS for cc: ${cc} found from main: ${initPTS}/${timescale}`,
141
+ );
142
+ this.videoAnchor = frag;
136
143
  // If we are waiting, tick immediately to unblock audio fragment transmuxing
137
144
  if (this.state === State.WAITING_INIT_PTS) {
145
+ const waitingData = this.waitingData;
146
+ if (!waitingData || waitingData.frag.cc !== cc) {
147
+ this.nextLoadPosition = this.findSyncFrag(frag).start;
148
+ }
138
149
  this.tick();
150
+ } else if (
151
+ !this.loadedmetadata &&
152
+ inFlightFrag &&
153
+ inFlightFrag.cc !== cc
154
+ ) {
155
+ this.startFragRequested = false;
156
+ this.nextLoadPosition = this.findSyncFrag(frag).start;
157
+ inFlightFrag.abortRequests();
158
+ this.resetLoadingState();
139
159
  }
140
160
  }
141
161
  }
142
162
 
163
+ private findSyncFrag(mainFrag: MediaFragment): MediaFragment {
164
+ const trackDetails = this.getLevelDetails();
165
+ const cc = mainFrag.cc;
166
+ return (
167
+ findNearestWithCC(trackDetails, cc, mainFrag) ||
168
+ (trackDetails && findFragWithCC(trackDetails.fragments, cc)) ||
169
+ mainFrag
170
+ );
171
+ }
172
+
143
173
  startLoad(startPosition: number) {
144
174
  if (!this.levels) {
145
175
  this.startPosition = startPosition;
@@ -202,9 +232,9 @@ class AudioStreamController
202
232
  const waitingData = this.waitingData;
203
233
  if (waitingData) {
204
234
  const { frag, part, cache, complete } = waitingData;
235
+ const videoAnchor = this.videoAnchor;
205
236
  if (this.initPTS[frag.cc] !== undefined) {
206
237
  this.waitingData = null;
207
- this.waitingVideoCC = -1;
208
238
  this.state = State.FRAG_LOADING;
209
239
  const payload = cache.flush();
210
240
  const data: FragLoadedData = {
@@ -217,33 +247,15 @@ class AudioStreamController
217
247
  if (complete) {
218
248
  super._handleFragmentLoadComplete(data);
219
249
  }
220
- } else if (this.videoTrackCC !== this.waitingVideoCC) {
250
+ } else if (videoAnchor && videoAnchor.cc !== waitingData.frag.cc) {
221
251
  // Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found
222
252
  this.log(
223
- `Waiting fragment cc (${frag.cc}) cancelled because video is at cc ${this.videoTrackCC}`,
253
+ `Waiting fragment cc (${frag.cc}) cancelled because video is at cc ${videoAnchor.cc}`,
224
254
  );
255
+ this.nextLoadPosition = this.findSyncFrag(videoAnchor).start;
225
256
  this.clearWaitingFragment();
226
- } else {
227
- // Drop waiting fragment if an earlier fragment is needed
228
- const pos = this.getLoadPosition();
229
- const bufferInfo = BufferHelper.bufferInfo(
230
- this.mediaBuffer,
231
- pos,
232
- this.config.maxBufferHole,
233
- );
234
- const waitingFragmentAtPosition = fragmentWithinToleranceTest(
235
- bufferInfo.end,
236
- this.config.maxFragLookUpTolerance,
237
- frag,
238
- );
239
- if (waitingFragmentAtPosition < 0) {
240
- this.log(
241
- `Waiting fragment cc (${frag.cc}) @ ${frag.start} cancelled because another fragment at ${bufferInfo.end} is needed`,
242
- );
243
- this.clearWaitingFragment();
244
- }
245
257
  }
246
- } else {
258
+ } else if (this.state !== State.STOPPED) {
247
259
  this.state = State.IDLE;
248
260
  }
249
261
  }
@@ -255,10 +267,15 @@ class AudioStreamController
255
267
  clearWaitingFragment() {
256
268
  const waitingData = this.waitingData;
257
269
  if (waitingData) {
270
+ if (!this.loadedmetadata) {
271
+ // Load overlapping fragment on start when discontinuity start times are not aligned
272
+ this.startFragRequested = false;
273
+ }
258
274
  this.fragmentTracker.removeFragment(waitingData.frag);
259
275
  this.waitingData = null;
260
- this.waitingVideoCC = -1;
261
- this.state = State.IDLE;
276
+ if (this.state !== State.STOPPED) {
277
+ this.state = State.IDLE;
278
+ }
262
279
  }
263
280
  }
264
281
 
@@ -281,12 +298,14 @@ class AudioStreamController
281
298
  const { hls, levels, media, trackId } = this;
282
299
  const config = hls.config;
283
300
 
284
- // 1. if video not attached AND
301
+ // 1. if buffering is suspended
302
+ // 2. if video not attached AND
285
303
  // start fragment already requested OR start frag prefetch not enabled
286
- // 2. if tracks or track not loaded and selected
304
+ // 3. if tracks or track not loaded and selected
287
305
  // then exit loop
288
306
  // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop
289
307
  if (
308
+ !this.buffering ||
290
309
  (!media && (this.startFragRequested || !config.startFragPrefetch)) ||
291
310
  !levels?.[trackId]
292
311
  ) {
@@ -330,21 +349,16 @@ class AudioStreamController
330
349
  return;
331
350
  }
332
351
 
333
- const mainBufferInfo = this.getFwdBufferInfo(
334
- this.videoBuffer ? this.videoBuffer : this.media,
335
- PlaylistLevelType.MAIN,
336
- );
337
352
  const bufferLen = bufferInfo.len;
338
- const maxBufLen = this.getMaxBufferLength(mainBufferInfo?.len);
353
+ const maxBufLen = hls.maxBufferLength;
339
354
 
340
355
  const fragments = trackDetails.fragments;
341
356
  const start = fragments[0].start;
342
- let targetBufferTime = this.flushing
343
- ? this.getLoadPosition()
344
- : bufferInfo.end;
357
+ const loadPosition = this.getLoadPosition();
358
+ let targetBufferTime = this.flushing ? loadPosition : bufferInfo.end;
345
359
 
346
360
  if (switchingTrack && media) {
347
- const pos = this.getLoadPosition();
361
+ const pos = loadPosition;
348
362
  // STABLE
349
363
  if (
350
364
  bufferedTrack &&
@@ -374,10 +388,8 @@ class AudioStreamController
374
388
  }
375
389
 
376
390
  let frag = this.getNextFragment(targetBufferTime, trackDetails);
377
- let atGap = false;
378
391
  // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags
379
392
  if (frag && this.isLoopLoading(frag, targetBufferTime)) {
380
- atGap = !!frag.gap;
381
393
  frag = this.getNextFragmentLoopLoading(
382
394
  frag,
383
395
  trackDetails,
@@ -391,27 +403,34 @@ class AudioStreamController
391
403
  return;
392
404
  }
393
405
 
394
- // Buffer audio up to one target duration ahead of main buffer
395
- const atBufferSyncLimit =
396
- mainBufferInfo &&
397
- frag.start > mainBufferInfo.end + trackDetails.targetduration;
406
+ // Request audio segments up to one fragment ahead of main stream-controller
407
+ const mainFragLoading = this.mainFragLoading?.frag;
398
408
  if (
399
- atBufferSyncLimit ||
400
- // Or wait for main buffer after buffing some audio
401
- (!mainBufferInfo?.len && bufferInfo.len)
409
+ this.startFragRequested &&
410
+ mainFragLoading &&
411
+ mainFragLoading.sn !== 'initSegment' &&
412
+ frag.sn !== 'initSegment' &&
413
+ !frag.endList &&
414
+ (!trackDetails.live ||
415
+ (!this.loadingParts && targetBufferTime < this.hls.liveSyncPosition!))
402
416
  ) {
403
- // Check fragment-tracker for main fragments since GAP segments do not show up in bufferInfo
404
- const mainFrag = this.getAppendedFrag(frag.start, PlaylistLevelType.MAIN);
405
- if (mainFrag === null) {
406
- return;
417
+ let mainFrag = mainFragLoading;
418
+ if (frag.start > mainFrag.end) {
419
+ // Get buffered frag at target position from tracker (loaded out of sequence)
420
+ const mainFragAtPos = this.fragmentTracker.getFragAtPos(
421
+ targetBufferTime,
422
+ PlaylistLevelType.MAIN,
423
+ );
424
+ if (mainFragAtPos && mainFragAtPos.end > mainFragLoading.end) {
425
+ mainFrag = mainFragAtPos;
426
+ this.mainFragLoading = {
427
+ frag: mainFragAtPos,
428
+ targetBufferTime: null,
429
+ };
430
+ }
407
431
  }
408
- // Bridge gaps in main buffer
409
- atGap ||=
410
- !!mainFrag.gap || (!!atBufferSyncLimit && mainBufferInfo.len === 0);
411
- if (
412
- (atBufferSyncLimit && !atGap) ||
413
- (atGap && bufferInfo.nextStart && bufferInfo.nextStart < mainFrag.end)
414
- ) {
432
+ const atBufferSyncLimit = frag.start > mainFrag.end;
433
+ if (atBufferSyncLimit) {
415
434
  return;
416
435
  }
417
436
  }
@@ -419,24 +438,12 @@ class AudioStreamController
419
438
  this.loadFragment(frag, levelInfo, targetBufferTime);
420
439
  }
421
440
 
422
- protected getMaxBufferLength(mainBufferLength?: number): number {
423
- const maxConfigBuffer = super.getMaxBufferLength();
424
- if (!mainBufferLength) {
425
- return maxConfigBuffer;
426
- }
427
- return Math.min(
428
- Math.max(maxConfigBuffer, mainBufferLength),
429
- this.config.maxMaxBufferLength,
430
- );
431
- }
432
-
433
- onMediaDetaching() {
434
- this.videoBuffer = null;
441
+ protected onMediaDetaching() {
435
442
  this.bufferFlushed = this.flushing = false;
436
443
  super.onMediaDetaching();
437
444
  }
438
445
 
439
- onAudioTracksUpdated(
446
+ private onAudioTracksUpdated(
440
447
  event: Events.AUDIO_TRACKS_UPDATED,
441
448
  { audioTracks }: AudioTracksUpdatedData,
442
449
  ) {
@@ -445,7 +452,7 @@ class AudioStreamController
445
452
  this.levels = audioTracks.map((mediaPlaylist) => new Level(mediaPlaylist));
446
453
  }
447
454
 
448
- onAudioTrackSwitching(
455
+ private onAudioTrackSwitching(
449
456
  event: Events.AUDIO_TRACK_SWITCHING,
450
457
  data: AudioTrackSwitchingData,
451
458
  ) {
@@ -459,44 +466,41 @@ class AudioStreamController
459
466
  this.removeUnbufferedFrags(fragCurrent.start);
460
467
  }
461
468
  this.resetLoadingState();
462
- // destroy useless transmuxer when switching audio to main
463
- if (!altAudio) {
464
- this.resetTransmuxer();
465
- } else {
466
- // switching to audio track, start timer if not already started
467
- this.setInterval(TICK_INTERVAL);
468
- }
469
469
 
470
470
  // should we switch tracks ?
471
471
  if (altAudio) {
472
472
  this.switchingTrack = data;
473
473
  // main audio track are handled by stream-controller, just do something if switching to alt audio track
474
- this.state = State.IDLE;
475
474
  this.flushAudioIfNeeded(data);
475
+ if (this.state !== State.STOPPED) {
476
+ // switching to audio track, start timer if not already started
477
+ this.setInterval(TICK_INTERVAL);
478
+ this.state = State.IDLE;
479
+ this.tick();
480
+ }
476
481
  } else {
482
+ // destroy useless transmuxer when switching audio to main
483
+ this.resetTransmuxer();
477
484
  this.switchingTrack = null;
478
485
  this.bufferedTrack = data;
479
- this.state = State.STOPPED;
486
+ this.clearInterval();
480
487
  }
481
- this.tick();
482
488
  }
483
489
 
484
- onManifestLoading() {
485
- this.fragmentTracker.removeAllFragments();
486
- this.startPosition = this.lastCurrentTime = 0;
490
+ protected onManifestLoading() {
491
+ super.onManifestLoading();
487
492
  this.bufferFlushed = this.flushing = false;
488
- this.levels =
489
- this.mainDetails =
493
+ this.mainDetails =
490
494
  this.waitingData =
495
+ this.videoAnchor =
491
496
  this.bufferedTrack =
492
497
  this.cachedTrackLoadedData =
493
498
  this.switchingTrack =
494
499
  null;
495
- this.startFragRequested = false;
496
- this.trackId = this.videoTrackCC = this.waitingVideoCC = -1;
500
+ this.trackId = -1;
497
501
  }
498
502
 
499
- onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
503
+ private onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
500
504
  this.mainDetails = data.details;
501
505
  if (this.cachedTrackLoadedData !== null) {
502
506
  this.hls.trigger(Events.AUDIO_TRACK_LOADED, this.cachedTrackLoadedData);
@@ -504,7 +508,10 @@ class AudioStreamController
504
508
  }
505
509
  }
506
510
 
507
- onAudioTrackLoaded(event: Events.AUDIO_TRACK_LOADED, data: TrackLoadedData) {
511
+ private onAudioTrackLoaded(
512
+ event: Events.AUDIO_TRACK_LOADED,
513
+ data: TrackLoadedData,
514
+ ) {
508
515
  if (this.mainDetails == null) {
509
516
  this.cachedTrackLoadedData = data;
510
517
  return;
@@ -570,7 +577,8 @@ class AudioStreamController
570
577
  }
571
578
 
572
579
  _handleFragmentLoadProgress(data: FragLoadedData) {
573
- const { frag, part, payload } = data;
580
+ const frag = data.frag as MediaFragment;
581
+ const { part, payload } = data;
574
582
  const { config, trackId, levels } = this;
575
583
  if (!levels) {
576
584
  this.warn(
@@ -615,7 +623,7 @@ class AudioStreamController
615
623
  const partial = partIndex !== -1;
616
624
  const chunkMeta = new ChunkMetadata(
617
625
  frag.level,
618
- frag.sn as number,
626
+ frag.sn,
619
627
  frag.stats.chunkCount,
620
628
  payload.byteLength,
621
629
  partIndex,
@@ -644,7 +652,6 @@ class AudioStreamController
644
652
  complete: false,
645
653
  });
646
654
  cache.push(new Uint8Array(payload));
647
- this.waitingVideoCC = this.videoTrackCC;
648
655
  this.state = State.WAITING_INIT_PTS;
649
656
  }
650
657
  }
@@ -657,32 +664,44 @@ class AudioStreamController
657
664
  super._handleFragmentLoadComplete(fragLoadedData);
658
665
  }
659
666
 
660
- onBufferReset(/* event: Events.BUFFER_RESET */) {
667
+ private onBufferReset(/* event: Events.BUFFER_RESET */) {
661
668
  // reset reference to sourcebuffers
662
- this.mediaBuffer = this.videoBuffer = null;
669
+ this.mediaBuffer = null;
663
670
  this.loadedmetadata = false;
664
671
  }
665
672
 
666
- onBufferCreated(event: Events.BUFFER_CREATED, data: BufferCreatedData) {
673
+ private onBufferCreated(
674
+ event: Events.BUFFER_CREATED,
675
+ data: BufferCreatedData,
676
+ ) {
667
677
  const audioTrack = data.tracks.audio;
668
678
  if (audioTrack) {
669
679
  this.mediaBuffer = audioTrack.buffer || null;
670
680
  }
671
- if (data.tracks.video) {
672
- this.videoBuffer = data.tracks.video.buffer || null;
681
+ }
682
+
683
+ private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
684
+ if (
685
+ data.frag.type === PlaylistLevelType.MAIN &&
686
+ data.frag.sn !== 'initSegment'
687
+ ) {
688
+ this.mainFragLoading = data;
689
+ if (this.state === State.IDLE) {
690
+ this.tick();
691
+ }
673
692
  }
674
693
  }
675
694
 
676
- onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
695
+ private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
677
696
  const { frag, part } = data;
678
697
  if (frag.type !== PlaylistLevelType.AUDIO) {
679
698
  if (!this.loadedmetadata && frag.type === PlaylistLevelType.MAIN) {
680
- const bufferable = this.videoBuffer || this.media;
681
- if (bufferable) {
682
- const bufferedTimeRanges = BufferHelper.getBuffered(bufferable);
683
- if (bufferedTimeRanges.length) {
684
- this.loadedmetadata = true;
685
- }
699
+ const bufferedState = this.fragmentTracker.getState(frag);
700
+ if (
701
+ bufferedState === FragmentState.OK ||
702
+ bufferedState === FragmentState.PARTIAL
703
+ ) {
704
+ this.loadedmetadata = true;
686
705
  }
687
706
  }
688
707
  return;
@@ -702,7 +721,7 @@ class AudioStreamController
702
721
  return;
703
722
  }
704
723
  if (frag.sn !== 'initSegment') {
705
- this.fragPrevious = frag;
724
+ this.fragPrevious = frag as MediaFragment;
706
725
  const track = this.switchingTrack;
707
726
  if (track) {
708
727
  this.bufferedTrack = track;
@@ -713,7 +732,7 @@ class AudioStreamController
713
732
  this.fragBufferedComplete(frag, part);
714
733
  }
715
734
 
716
- private onError(event: Events.ERROR, data: ErrorData) {
735
+ protected onError(event: Events.ERROR, data: ErrorData) {
717
736
  if (data.fatal) {
718
737
  this.state = State.ERROR;
719
738
  return;
@@ -887,12 +906,15 @@ class AudioStreamController
887
906
  if (tracks.video) {
888
907
  delete tracks.video;
889
908
  }
909
+ if (tracks.audiovideo) {
910
+ delete tracks.audiovideo;
911
+ }
890
912
 
891
913
  // include levelCodec in audio and video tracks
892
- const track = tracks.audio;
893
- if (!track) {
914
+ if (!tracks.audio) {
894
915
  return;
895
916
  }
917
+ const track = tracks.audio;
896
918
 
897
919
  track.id = 'audio';
898
920
 
@@ -904,7 +926,7 @@ class AudioStreamController
904
926
  if (variantAudioCodecs && variantAudioCodecs.split(',').length === 1) {
905
927
  track.levelCodec = variantAudioCodecs;
906
928
  }
907
- this.hls.trigger(Events.BUFFER_CODECS, tracks);
929
+ this.hls.trigger(Events.BUFFER_CODECS, tracks as BufferCodecsData);
908
930
  const initSegment = track.initSegment;
909
931
  if (initSegment?.byteLength) {
910
932
  const segment: BufferAppendingData = {
@@ -928,7 +950,6 @@ class AudioStreamController
928
950
  ) {
929
951
  // only load if fragment is not loaded or if in audio switch
930
952
  const fragState = this.fragmentTracker.getState(frag);
931
- this.fragCurrent = frag;
932
953
 
933
954
  // we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch
934
955
  if (
@@ -951,7 +972,6 @@ class AudioStreamController
951
972
  alignMediaPlaylistByPDT(track.details, mainDetails);
952
973
  }
953
974
  } else {
954
- this.startFragRequested = true;
955
975
  super.loadFragment(frag, track, targetBufferTime);
956
976
  }
957
977
  } else {
@@ -33,7 +33,7 @@ class AudioTrackController extends BasePlaylistController {
33
33
  private selectDefaultTrack: boolean = true;
34
34
 
35
35
  constructor(hls: Hls) {
36
- super(hls, '[audio-track-controller]');
36
+ super(hls, 'audio-track-controller');
37
37
  this.registerListeners();
38
38
  }
39
39
 
@@ -1,11 +1,16 @@
1
1
  import type Hls from '../hls';
2
2
  import type { NetworkComponentAPI } from '../types/component-api';
3
- import { getSkipValue, HlsSkip, HlsUrlParameters, Level } from '../types/level';
3
+ import {
4
+ getSkipValue,
5
+ HlsSkip,
6
+ HlsUrlParameters,
7
+ type Level,
8
+ } from '../types/level';
4
9
  import { computeReloadInterval, mergeDetails } from '../utils/level-helper';
5
- import { ErrorData } from '../types/events';
10
+ import type { ErrorData } from '../types/events';
6
11
  import { getRetryDelay, isTimeoutError } from '../utils/error-helper';
7
12
  import { NetworkErrorAction } from './error-controller';
8
- import { logger } from '../utils/logger';
13
+ import { Logger } from '../utils/logger';
9
14
  import type { LevelDetails } from '../loader/level-details';
10
15
  import type { MediaPlaylist } from '../types/media-playlist';
11
16
  import type {
@@ -14,17 +19,17 @@ import type {
14
19
  TrackLoadedData,
15
20
  } from '../types/events';
16
21
 
17
- export default class BasePlaylistController implements NetworkComponentAPI {
22
+ export default class BasePlaylistController
23
+ extends Logger
24
+ implements NetworkComponentAPI
25
+ {
18
26
  protected hls: Hls;
19
27
  protected timer: number = -1;
20
28
  protected requestScheduled: number = -1;
21
29
  protected canLoad: boolean = false;
22
- protected log: (msg: any) => void;
23
- protected warn: (msg: any) => void;
24
30
 
25
31
  constructor(hls: Hls, logPrefix: string) {
26
- this.log = logger.log.bind(logger, `${logPrefix}:`);
27
- this.warn = logger.warn.bind(logger, `${logPrefix}:`);
32
+ super(logPrefix, hls.logger);
28
33
  this.hls = hls;
29
34
  }
30
35
 
@@ -66,7 +71,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
66
71
  try {
67
72
  uri = new self.URL(attr.URI, previous.url).href;
68
73
  } catch (error) {
69
- logger.warn(
74
+ this.warn(
70
75
  `Could not construct new URL for Rendition Report: ${error}`,
71
76
  );
72
77
  uri = attr.URI || '';
@@ -190,7 +195,19 @@ export default class BasePlaylistController implements NetworkComponentAPI {
190
195
  details.targetduration * 1.5,
191
196
  );
192
197
  if (currentGoal > 0) {
193
- if (previousDetails && currentGoal > previousDetails.tuneInGoal) {
198
+ if (cdnAge > details.targetduration * 3) {
199
+ // Omit segment and part directives when the last response was more than 3 target durations ago,
200
+ this.log(
201
+ `Playlist last advanced ${lastAdvanced.toFixed(
202
+ 2,
203
+ )}s ago. Omitting segment and part directives.`,
204
+ );
205
+ msn = undefined;
206
+ part = undefined;
207
+ } else if (
208
+ previousDetails?.tuneInGoal &&
209
+ cdnAge - details.partTarget > previousDetails.tuneInGoal
210
+ ) {
194
211
  // If we attempted to get the next or latest playlist update, but currentGoal increased,
195
212
  // then we either can't catchup, or the "age" header cannot be trusted.
196
213
  this.warn(