hls.js 1.5.9 → 1.5.10-0.canary.10320

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 (89) 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 +3477 -2194
  5. package/dist/hls.js.d.ts +108 -85
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2401 -1754
  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 +1989 -1315
  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 +2863 -1557
  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 +35 -35
  20. package/src/config.ts +3 -2
  21. package/src/controller/abr-controller.ts +24 -20
  22. package/src/controller/audio-stream-controller.ts +68 -74
  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 +160 -38
  26. package/src/controller/buffer-controller.ts +230 -92
  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 +6 -8
  33. package/src/controller/fps-controller.ts +8 -3
  34. package/src/controller/fragment-tracker.ts +15 -11
  35. package/src/controller/gap-controller.ts +43 -16
  36. package/src/controller/id3-track-controller.ts +7 -7
  37. package/src/controller/latency-controller.ts +9 -11
  38. package/src/controller/level-controller.ts +37 -19
  39. package/src/controller/stream-controller.ts +37 -32
  40. package/src/controller/subtitle-stream-controller.ts +28 -40
  41. package/src/controller/subtitle-track-controller.ts +5 -3
  42. package/src/controller/timeline-controller.ts +19 -21
  43. package/src/crypt/aes-crypto.ts +21 -2
  44. package/src/crypt/decrypter-aes-mode.ts +4 -0
  45. package/src/crypt/decrypter.ts +32 -16
  46. package/src/crypt/fast-aes-key.ts +28 -5
  47. package/src/demux/audio/aacdemuxer.ts +2 -2
  48. package/src/demux/audio/ac3-demuxer.ts +4 -3
  49. package/src/demux/audio/adts.ts +9 -4
  50. package/src/demux/audio/base-audio-demuxer.ts +16 -14
  51. package/src/demux/audio/mp3demuxer.ts +4 -3
  52. package/src/demux/audio/mpegaudio.ts +1 -1
  53. package/src/demux/mp4demuxer.ts +7 -7
  54. package/src/demux/sample-aes.ts +2 -0
  55. package/src/demux/transmuxer-interface.ts +4 -12
  56. package/src/demux/transmuxer-worker.ts +4 -4
  57. package/src/demux/transmuxer.ts +16 -3
  58. package/src/demux/tsdemuxer.ts +71 -37
  59. package/src/demux/video/avc-video-parser.ts +208 -119
  60. package/src/demux/video/base-video-parser.ts +147 -18
  61. package/src/demux/video/exp-golomb.ts +0 -208
  62. package/src/demux/video/hevc-video-parser.ts +749 -0
  63. package/src/empty-es.js +5 -0
  64. package/src/events.ts +8 -1
  65. package/src/exports-named.ts +1 -1
  66. package/src/hls.ts +61 -38
  67. package/src/loader/fragment-loader.ts +10 -3
  68. package/src/loader/key-loader.ts +3 -1
  69. package/src/loader/level-key.ts +10 -9
  70. package/src/loader/playlist-loader.ts +4 -5
  71. package/src/remux/mp4-generator.ts +196 -1
  72. package/src/remux/mp4-remuxer.ts +24 -8
  73. package/src/task-loop.ts +5 -2
  74. package/src/types/component-api.ts +3 -1
  75. package/src/types/demuxer.ts +4 -0
  76. package/src/types/events.ts +4 -0
  77. package/src/types/remuxer.ts +1 -1
  78. package/src/utils/buffer-helper.ts +12 -31
  79. package/src/utils/cea-608-parser.ts +1 -3
  80. package/src/utils/codecs.ts +34 -5
  81. package/src/utils/encryption-methods-util.ts +21 -0
  82. package/src/utils/fetch-loader.ts +1 -1
  83. package/src/utils/imsc1-ttml-parser.ts +1 -1
  84. package/src/utils/keysystem-util.ts +1 -6
  85. package/src/utils/logger.ts +58 -23
  86. package/src/utils/mp4-tools.ts +5 -3
  87. package/src/utils/utf8-utils.ts +18 -0
  88. package/src/utils/webvtt-parser.ts +1 -1
  89. package/src/demux/id3.ts +0 -411
package/package.json CHANGED
@@ -58,52 +58,52 @@
58
58
  "test:func:sauce": "SAUCE=1 UA=safari OS='OS X 10.15' BABEL_ENV=development mocha --require @babel/register tests/functional/auto/setup.js --timeout 40000 --exit",
59
59
  "type-check": "tsc --noEmit",
60
60
  "type-check:watch": "npm run type-check -- --watch",
61
- "prepare": "husky install"
61
+ "prepare": "husky"
62
62
  },
63
63
  "devDependencies": {
64
- "@babel/core": "7.23.7",
65
- "@babel/helper-module-imports": "7.22.15",
64
+ "@babel/core": "7.24.6",
65
+ "@babel/helper-module-imports": "7.24.6",
66
66
  "@babel/plugin-proposal-class-properties": "7.18.6",
67
67
  "@babel/plugin-proposal-object-rest-spread": "7.20.7",
68
68
  "@babel/plugin-proposal-optional-chaining": "7.21.0",
69
- "@babel/plugin-transform-object-assign": "7.23.3",
70
- "@babel/preset-env": "7.23.7",
71
- "@babel/preset-typescript": "7.23.3",
72
- "@babel/register": "7.23.7",
73
- "@microsoft/api-documenter": "7.23.16",
74
- "@microsoft/api-extractor": "7.39.1",
69
+ "@babel/plugin-transform-object-assign": "7.24.6",
70
+ "@babel/preset-env": "7.24.6",
71
+ "@babel/preset-typescript": "7.24.6",
72
+ "@babel/register": "7.24.6",
73
+ "@microsoft/api-documenter": "7.25.2",
74
+ "@microsoft/api-extractor": "7.46.2",
75
75
  "@rollup/plugin-alias": "5.1.0",
76
76
  "@rollup/plugin-babel": "6.0.4",
77
- "@rollup/plugin-commonjs": "25.0.7",
77
+ "@rollup/plugin-commonjs": "25.0.8",
78
78
  "@rollup/plugin-node-resolve": "15.2.3",
79
79
  "@rollup/plugin-replace": "5.0.5",
80
80
  "@rollup/plugin-terser": "0.4.4",
81
- "@rollup/plugin-typescript": "11.1.5",
82
- "@svta/common-media-library": "0.6.1",
83
- "@types/chai": "4.3.11",
81
+ "@rollup/plugin-typescript": "11.1.6",
82
+ "@svta/common-media-library": "0.6.4",
83
+ "@types/chai": "4.3.16",
84
84
  "@types/chart.js": "2.9.41",
85
85
  "@types/mocha": "10.0.6",
86
86
  "@types/sinon-chai": "3.2.12",
87
- "@typescript-eslint/eslint-plugin": "6.17.0",
88
- "@typescript-eslint/parser": "6.17.0",
87
+ "@typescript-eslint/eslint-plugin": "7.10.0",
88
+ "@typescript-eslint/parser": "7.10.0",
89
89
  "babel-loader": "9.1.3",
90
90
  "babel-plugin-transform-remove-console": "6.9.4",
91
- "chai": "4.3.10",
91
+ "chai": "4.4.1",
92
92
  "chart.js": "2.9.4",
93
- "chromedriver": "120.0.1",
93
+ "chromedriver": "125.0.2",
94
94
  "doctoc": "2.2.1",
95
- "es-check": "7.1.1",
96
- "eslint": "8.56.0",
95
+ "es-check": "7.2.1",
96
+ "eslint": "8.57.0",
97
97
  "eslint-config-prettier": "9.1.0",
98
98
  "eslint-plugin-import": "2.29.1",
99
- "eslint-plugin-mocha": "10.2.0",
100
- "eslint-plugin-node": "11.1.0",
99
+ "eslint-plugin-mocha": "10.4.3",
100
+ "eslint-plugin-n": "17.7.0",
101
101
  "eslint-plugin-promise": "6.1.1",
102
102
  "eventemitter3": "5.0.1",
103
103
  "http-server": "14.1.1",
104
- "husky": "8.0.3",
104
+ "husky": "9.0.11",
105
105
  "jsonpack": "1.1.5",
106
- "karma": "6.4.2",
106
+ "karma": "6.4.3",
107
107
  "karma-chrome-launcher": "3.2.0",
108
108
  "karma-coverage": "2.2.1",
109
109
  "karma-mocha": "2.0.1",
@@ -111,24 +111,24 @@
111
111
  "karma-rollup-preprocessor": "github:jlmakes/karma-rollup-preprocessor#7a7268d91149307b3cf2888ee4e65ccd079955a3",
112
112
  "karma-sinon-chai": "2.0.2",
113
113
  "karma-sourcemap-loader": "0.4.0",
114
- "lint-staged": "15.2.0",
114
+ "lint-staged": "15.2.5",
115
115
  "markdown-styles": "3.2.0",
116
- "micromatch": "4.0.5",
117
- "mocha": "10.2.0",
116
+ "micromatch": "4.0.7",
117
+ "mocha": "10.4.0",
118
118
  "node-fetch": "3.3.2",
119
- "npm-run-all": "4.1.5",
120
- "prettier": "3.1.1",
119
+ "npm-run-all2": "6.2.0",
120
+ "prettier": "3.2.5",
121
121
  "promise-polyfill": "8.3.0",
122
- "rollup": "4.9.4",
122
+ "rollup": "4.18.0",
123
123
  "rollup-plugin-istanbul": "5.0.0",
124
124
  "sauce-connect-launcher": "1.3.2",
125
- "selenium-webdriver": "4.16.0",
126
- "semver": "7.5.4",
127
- "sinon": "17.0.1",
125
+ "selenium-webdriver": "4.21.0",
126
+ "semver": "7.6.2",
127
+ "sinon": "18.0.0",
128
128
  "sinon-chai": "3.7.0",
129
- "typescript": "5.3.3",
129
+ "typescript": "5.4.5",
130
130
  "url-toolkit": "2.2.5",
131
- "wrangler": "3.22.4"
131
+ "wrangler": "3.57.1"
132
132
  },
133
- "version": "1.5.9"
133
+ "version": "1.5.10-0.canary.10320"
134
134
  }
package/src/config.ts CHANGED
@@ -17,10 +17,10 @@ import XhrLoader from './utils/xhr-loader';
17
17
  import FetchLoader, { fetchSupported } from './utils/fetch-loader';
18
18
  import Cues from './utils/cues';
19
19
  import { requestMediaKeySystemAccess } from './utils/mediakeys-helper';
20
- import { ILogger, logger } from './utils/logger';
21
20
 
22
21
  import type Hls from './hls';
23
22
  import type { CuesInterface } from './utils/cues';
23
+ import type { ILogger } from './utils/logger';
24
24
  import type { MediaKeyFunc, KeySystems } from './utils/mediakeys-helper';
25
25
  import type {
26
26
  FragmentLoaderContext,
@@ -558,6 +558,7 @@ function timelineConfig(): TimelineControllerConfig {
558
558
  export function mergeConfig(
559
559
  defaultConfig: HlsConfig,
560
560
  userConfig: Partial<HlsConfig>,
561
+ logger: ILogger,
561
562
  ): HlsConfig {
562
563
  if (
563
564
  (userConfig.liveSyncDurationCount ||
@@ -664,7 +665,7 @@ function deepCpy(obj: any): any {
664
665
  /**
665
666
  * @ignore
666
667
  */
667
- export function enableStreamingMode(config) {
668
+ export function enableStreamingMode(config: HlsConfig, logger: ILogger) {
668
669
  const currentLoader = config.loader;
669
670
  if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) {
670
671
  // If a developer has configured their own loader, respect that choice
@@ -2,7 +2,7 @@ import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
2
2
  import { Events } from '../events';
3
3
  import { ErrorDetails } from '../errors';
4
4
  import { PlaylistLevelType } from '../types/loader';
5
- import { logger } from '../utils/logger';
5
+ import { Logger } from '../utils/logger';
6
6
  import {
7
7
  SUPPORTED_INFO_DEFAULT,
8
8
  getMediaDecodingInfoPromise,
@@ -31,7 +31,7 @@ import type {
31
31
  } from '../types/events';
32
32
  import type { AbrComponentAPI } from '../types/component-api';
33
33
 
34
- class AbrController implements AbrComponentAPI {
34
+ class AbrController extends Logger implements AbrComponentAPI {
35
35
  protected hls: Hls;
36
36
  private lastLevelLoadSec: number = 0;
37
37
  private lastLoadedFragLevel: number = -1;
@@ -48,6 +48,7 @@ class AbrController implements AbrComponentAPI {
48
48
  public bwEstimator: EwmaBandWidthEstimator;
49
49
 
50
50
  constructor(hls: Hls) {
51
+ super('abr', hls.logger);
51
52
  this.hls = hls;
52
53
  this.bwEstimator = this.initEstimator();
53
54
  this.registerListeners();
@@ -55,7 +56,7 @@ class AbrController implements AbrComponentAPI {
55
56
 
56
57
  public resetEstimator(abrEwmaDefaultEstimate?: number) {
57
58
  if (abrEwmaDefaultEstimate) {
58
- logger.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
59
+ this.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
59
60
  this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate;
60
61
  }
61
62
  this.firstSelection = -1;
@@ -355,7 +356,7 @@ class AbrController implements AbrComponentAPI {
355
356
  }
356
357
 
357
358
  this.clearTimer();
358
- logger.warn(`[abr] Fragment ${frag.sn}${
359
+ this.warn(`Fragment ${frag.sn}${
359
360
  part ? ' part ' + part.index : ''
360
361
  } of level ${frag.level} is loading too slowly;
361
362
  Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s
@@ -479,8 +480,8 @@ class AbrController implements AbrComponentAPI {
479
480
  }
480
481
  const firstLevel = this.hls.firstLevel;
481
482
  const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel);
482
- logger.warn(
483
- `[abr] Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`,
483
+ this.warn(
484
+ `Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`,
484
485
  );
485
486
  return clamped;
486
487
  }
@@ -538,6 +539,9 @@ class AbrController implements AbrComponentAPI {
538
539
 
539
540
  private getNextABRAutoLevel(): number {
540
541
  const { fragCurrent, partCurrent, hls } = this;
542
+ if (hls.levels.length <= 1) {
543
+ return hls.loadLevel;
544
+ }
541
545
  const { maxAutoLevel, config, minAutoLevel } = hls;
542
546
  const currentFragDuration = partCurrent
543
547
  ? partCurrent.duration
@@ -584,8 +588,8 @@ class AbrController implements AbrComponentAPI {
584
588
  ? Math.min(currentFragDuration, config.maxLoadingDelay)
585
589
  : config.maxLoadingDelay;
586
590
  maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
587
- logger.info(
588
- `[abr] bitrate test took ${Math.round(
591
+ this.info(
592
+ `bitrate test took ${Math.round(
589
593
  1000 * bitrateTestDelay,
590
594
  )}ms, set first fragment max fetchDuration to ${Math.round(
591
595
  1000 * maxStarvationDelay,
@@ -604,8 +608,8 @@ class AbrController implements AbrComponentAPI {
604
608
  bwFactor,
605
609
  bwUpFactor,
606
610
  );
607
- logger.info(
608
- `[abr] ${
611
+ this.info(
612
+ `${
609
613
  bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'
610
614
  }, optimal quality level ${bestLevel}`,
611
615
  );
@@ -698,7 +702,7 @@ class AbrController implements AbrComponentAPI {
698
702
  : videoRanges[0];
699
703
  currentFrameRate = minFramerate;
700
704
  currentBw = Math.max(currentBw, minBitrate);
701
- logger.log(`[abr] picked start tier ${JSON.stringify(startTier)}`);
705
+ this.log(`picked start tier ${JSON.stringify(startTier)}`);
702
706
  } else {
703
707
  currentCodecSet = level?.codecSet;
704
708
  currentVideoRange = level?.videoRange;
@@ -751,19 +755,19 @@ class AbrController implements AbrComponentAPI {
751
755
  const levels = this.hls.levels;
752
756
  const index = levels.indexOf(levelInfo);
753
757
  if (decodingInfo.error) {
754
- logger.warn(
755
- `[abr] MediaCapabilities decodingInfo error: "${
758
+ this.warn(
759
+ `MediaCapabilities decodingInfo error: "${
756
760
  decodingInfo.error
757
761
  }" for level ${index} ${JSON.stringify(decodingInfo)}`,
758
762
  );
759
763
  } else if (!decodingInfo.supported) {
760
- logger.warn(
761
- `[abr] Unsupported MediaCapabilities decodingInfo result for level ${index} ${JSON.stringify(
764
+ this.warn(
765
+ `Unsupported MediaCapabilities decodingInfo result for level ${index} ${JSON.stringify(
762
766
  decodingInfo,
763
767
  )}`,
764
768
  );
765
769
  if (index > -1 && levels.length > 1) {
766
- logger.log(`[abr] Removing unsupported level ${index}`);
770
+ this.log(`Removing unsupported level ${index}`);
767
771
  this.hls.removeLevel(index);
768
772
  }
769
773
  }
@@ -842,8 +846,8 @@ class AbrController implements AbrComponentAPI {
842
846
  (forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)
843
847
  ) {
844
848
  if (levelsSkipped.length) {
845
- logger.trace(
846
- `[abr] Skipped level(s) ${levelsSkipped.join(
849
+ this.trace(
850
+ `Skipped level(s) ${levelsSkipped.join(
847
851
  ',',
848
852
  )} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${
849
853
  levels[levelsSkipped[0]].codecs
@@ -852,8 +856,8 @@ class AbrController implements AbrComponentAPI {
852
856
  }" ${currentVideoRange}`,
853
857
  );
854
858
  }
855
- logger.info(
856
- `[abr] switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(
859
+ this.info(
860
+ `switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(
857
861
  adjustedbw,
858
862
  )})-bitrate=${Math.round(
859
863
  adjustedbw - bitrate,
@@ -71,30 +71,27 @@ class AudioStreamController
71
71
  hls,
72
72
  fragmentTracker,
73
73
  keyLoader,
74
- '[audio-stream-controller]',
74
+ 'audio-stream-controller',
75
75
  PlaylistLevelType.AUDIO,
76
76
  );
77
- this._registerListeners();
77
+ this.registerListeners();
78
78
  }
79
79
 
80
80
  protected onHandlerDestroying() {
81
- this._unregisterListeners();
81
+ this.unregisterListeners();
82
82
  super.onHandlerDestroying();
83
83
  this.mainDetails = null;
84
84
  this.bufferedTrack = null;
85
85
  this.switchingTrack = null;
86
86
  }
87
87
 
88
- private _registerListeners() {
88
+ protected registerListeners() {
89
+ super.registerListeners();
89
90
  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
91
  hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
94
92
  hls.on(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this);
95
93
  hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
96
94
  hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
97
- hls.on(Events.ERROR, this.onError, this);
98
95
  hls.on(Events.BUFFER_RESET, this.onBufferReset, this);
99
96
  hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this);
100
97
  hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
@@ -103,16 +100,16 @@ class AudioStreamController
103
100
  hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
104
101
  }
105
102
 
106
- private _unregisterListeners() {
103
+ protected unregisterListeners() {
107
104
  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);
105
+ if (!hls) {
106
+ return;
107
+ }
108
+ super.unregisterListeners();
111
109
  hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
112
110
  hls.off(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this);
113
111
  hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
114
112
  hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
115
- hls.off(Events.ERROR, this.onError, this);
116
113
  hls.off(Events.BUFFER_RESET, this.onBufferReset, this);
117
114
  hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this);
118
115
  hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
@@ -258,7 +255,9 @@ class AudioStreamController
258
255
  this.fragmentTracker.removeFragment(waitingData.frag);
259
256
  this.waitingData = null;
260
257
  this.waitingVideoCC = -1;
261
- this.state = State.IDLE;
258
+ if (this.state !== State.STOPPED) {
259
+ this.state = State.IDLE;
260
+ }
262
261
  }
263
262
  }
264
263
 
@@ -281,12 +280,14 @@ class AudioStreamController
281
280
  const { hls, levels, media, trackId } = this;
282
281
  const config = hls.config;
283
282
 
284
- // 1. if video not attached AND
283
+ // 1. if buffering is suspended
284
+ // 2. if video not attached AND
285
285
  // start fragment already requested OR start frag prefetch not enabled
286
- // 2. if tracks or track not loaded and selected
286
+ // 3. if tracks or track not loaded and selected
287
287
  // then exit loop
288
288
  // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop
289
289
  if (
290
+ !this.buffering ||
290
291
  (!media && (this.startFragRequested || !config.startFragPrefetch)) ||
291
292
  !levels?.[trackId]
292
293
  ) {
@@ -330,12 +331,8 @@ class AudioStreamController
330
331
  return;
331
332
  }
332
333
 
333
- const mainBufferInfo = this.getFwdBufferInfo(
334
- this.videoBuffer ? this.videoBuffer : this.media,
335
- PlaylistLevelType.MAIN,
336
- );
337
334
  const bufferLen = bufferInfo.len;
338
- const maxBufLen = this.getMaxBufferLength(mainBufferInfo?.len);
335
+ const maxBufLen = hls.maxBufferLength;
339
336
 
340
337
  const fragments = trackDetails.fragments;
341
338
  const start = fragments[0].start;
@@ -391,52 +388,44 @@ class AudioStreamController
391
388
  return;
392
389
  }
393
390
 
394
- // Buffer audio up to one target duration ahead of main buffer
395
- const atBufferSyncLimit =
396
- mainBufferInfo &&
397
- frag.start > mainBufferInfo.end + trackDetails.targetduration;
398
- if (
399
- atBufferSyncLimit ||
400
- // Or wait for main buffer after buffing some audio
401
- (!mainBufferInfo?.len && bufferInfo.len)
402
- ) {
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;
407
- }
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
- ) {
415
- return;
391
+ if (!trackDetails.live || targetBufferTime < this.hls.liveSyncPosition!) {
392
+ // Request audio segments up to one fragment ahead of main buffer
393
+ const mainBufferInfo = this.getFwdBufferInfo(
394
+ this.videoBuffer ? this.videoBuffer : this.media,
395
+ PlaylistLevelType.MAIN,
396
+ );
397
+ const atBufferSyncLimit =
398
+ !!mainBufferInfo && frag.start > mainBufferInfo.end + frag.duration;
399
+ if (atBufferSyncLimit) {
400
+ // Check fragment-tracker for main fragments since GAP segments do not show up in bufferInfo
401
+ const mainFrag = this.fragmentTracker.getFragAtPos(
402
+ frag.start,
403
+ PlaylistLevelType.MAIN,
404
+ );
405
+ if (mainFrag === null) {
406
+ return;
407
+ }
408
+ // Bridge gaps in main buffer (also prevents loop loading at gaps)
409
+ atGap ||= !!mainFrag.gap || mainBufferInfo.len === 0;
410
+ if (
411
+ !atGap ||
412
+ (bufferInfo.nextStart && bufferInfo.nextStart < mainFrag.end)
413
+ ) {
414
+ return;
415
+ }
416
416
  }
417
417
  }
418
418
 
419
419
  this.loadFragment(frag, levelInfo, targetBufferTime);
420
420
  }
421
421
 
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() {
422
+ protected onMediaDetaching() {
434
423
  this.videoBuffer = null;
435
424
  this.bufferFlushed = this.flushing = false;
436
425
  super.onMediaDetaching();
437
426
  }
438
427
 
439
- onAudioTracksUpdated(
428
+ private onAudioTracksUpdated(
440
429
  event: Events.AUDIO_TRACKS_UPDATED,
441
430
  { audioTracks }: AudioTracksUpdatedData,
442
431
  ) {
@@ -445,7 +434,7 @@ class AudioStreamController
445
434
  this.levels = audioTracks.map((mediaPlaylist) => new Level(mediaPlaylist));
446
435
  }
447
436
 
448
- onAudioTrackSwitching(
437
+ private onAudioTrackSwitching(
449
438
  event: Events.AUDIO_TRACK_SWITCHING,
450
439
  data: AudioTrackSwitchingData,
451
440
  ) {
@@ -459,29 +448,28 @@ class AudioStreamController
459
448
  this.removeUnbufferedFrags(fragCurrent.start);
460
449
  }
461
450
  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
451
 
470
452
  // should we switch tracks ?
471
453
  if (altAudio) {
472
454
  this.switchingTrack = data;
473
455
  // main audio track are handled by stream-controller, just do something if switching to alt audio track
474
- this.state = State.IDLE;
475
456
  this.flushAudioIfNeeded(data);
457
+ if (this.state !== State.STOPPED) {
458
+ // switching to audio track, start timer if not already started
459
+ this.setInterval(TICK_INTERVAL);
460
+ this.state = State.IDLE;
461
+ this.tick();
462
+ }
476
463
  } else {
464
+ // destroy useless transmuxer when switching audio to main
465
+ this.resetTransmuxer();
477
466
  this.switchingTrack = null;
478
467
  this.bufferedTrack = data;
479
- this.state = State.STOPPED;
468
+ this.clearInterval();
480
469
  }
481
- this.tick();
482
470
  }
483
471
 
484
- onManifestLoading() {
472
+ protected onManifestLoading() {
485
473
  this.fragmentTracker.removeAllFragments();
486
474
  this.startPosition = this.lastCurrentTime = 0;
487
475
  this.bufferFlushed = this.flushing = false;
@@ -496,7 +484,7 @@ class AudioStreamController
496
484
  this.trackId = this.videoTrackCC = this.waitingVideoCC = -1;
497
485
  }
498
486
 
499
- onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
487
+ private onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
500
488
  this.mainDetails = data.details;
501
489
  if (this.cachedTrackLoadedData !== null) {
502
490
  this.hls.trigger(Events.AUDIO_TRACK_LOADED, this.cachedTrackLoadedData);
@@ -504,7 +492,10 @@ class AudioStreamController
504
492
  }
505
493
  }
506
494
 
507
- onAudioTrackLoaded(event: Events.AUDIO_TRACK_LOADED, data: TrackLoadedData) {
495
+ private onAudioTrackLoaded(
496
+ event: Events.AUDIO_TRACK_LOADED,
497
+ data: TrackLoadedData,
498
+ ) {
508
499
  if (this.mainDetails == null) {
509
500
  this.cachedTrackLoadedData = data;
510
501
  return;
@@ -657,13 +648,16 @@ class AudioStreamController
657
648
  super._handleFragmentLoadComplete(fragLoadedData);
658
649
  }
659
650
 
660
- onBufferReset(/* event: Events.BUFFER_RESET */) {
651
+ private onBufferReset(/* event: Events.BUFFER_RESET */) {
661
652
  // reset reference to sourcebuffers
662
653
  this.mediaBuffer = this.videoBuffer = null;
663
654
  this.loadedmetadata = false;
664
655
  }
665
656
 
666
- onBufferCreated(event: Events.BUFFER_CREATED, data: BufferCreatedData) {
657
+ private onBufferCreated(
658
+ event: Events.BUFFER_CREATED,
659
+ data: BufferCreatedData,
660
+ ) {
667
661
  const audioTrack = data.tracks.audio;
668
662
  if (audioTrack) {
669
663
  this.mediaBuffer = audioTrack.buffer || null;
@@ -673,7 +667,7 @@ class AudioStreamController
673
667
  }
674
668
  }
675
669
 
676
- onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
670
+ private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
677
671
  const { frag, part } = data;
678
672
  if (frag.type !== PlaylistLevelType.AUDIO) {
679
673
  if (!this.loadedmetadata && frag.type === PlaylistLevelType.MAIN) {
@@ -713,7 +707,7 @@ class AudioStreamController
713
707
  this.fragBufferedComplete(frag, part);
714
708
  }
715
709
 
716
- private onError(event: Events.ERROR, data: ErrorData) {
710
+ protected onError(event: Events.ERROR, data: ErrorData) {
717
711
  if (data.fatal) {
718
712
  this.state = State.ERROR;
719
713
  return;
@@ -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(