@zenvor/hls.js 1.0.0

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 (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,622 @@
1
+ import { ErrorActionFlags, NetworkErrorAction } from './error-controller';
2
+ import { ErrorDetails } from '../errors';
3
+ import { Events } from '../events';
4
+ import { Level } from '../types/level';
5
+ import {
6
+ type Loader,
7
+ type LoaderCallbacks,
8
+ type LoaderConfiguration,
9
+ type LoaderContext,
10
+ type LoaderResponse,
11
+ type LoaderStats,
12
+ PlaylistContextType,
13
+ } from '../types/loader';
14
+ import { AttrList } from '../utils/attr-list';
15
+ import { reassignFragmentLevelIndexes } from '../utils/level-helper';
16
+ import { Logger } from '../utils/logger';
17
+ import { stringify } from '../utils/safe-json-stringify';
18
+ import type { RetryConfig } from '../config';
19
+ import type Hls from '../hls';
20
+ import type { NetworkComponentAPI } from '../types/component-api';
21
+ import type {
22
+ ErrorData,
23
+ ManifestLoadedData,
24
+ ManifestParsedData,
25
+ SteeringManifestLoadedData,
26
+ } from '../types/events';
27
+ import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
28
+ import type { NullableNetworkDetails } from '../types/network-details';
29
+
30
+ export type SteeringManifest = {
31
+ VERSION: 1;
32
+ TTL: number;
33
+ 'RELOAD-URI'?: string;
34
+ 'PATHWAY-PRIORITY': string[];
35
+ 'PATHWAY-CLONES'?: PathwayClone[];
36
+ };
37
+
38
+ export type PathwayClone = {
39
+ 'BASE-ID': string;
40
+ ID: string;
41
+ 'URI-REPLACEMENT': UriReplacement;
42
+ };
43
+
44
+ export type UriReplacement = {
45
+ HOST?: string;
46
+ PARAMS?: { [queryParameter: string]: string };
47
+ 'PER-VARIANT-URIS'?: { [stableVariantId: string]: string };
48
+ 'PER-RENDITION-URIS'?: { [stableRenditionId: string]: string };
49
+ };
50
+
51
+ const PATHWAY_PENALTY_DURATION_MS = 300000;
52
+
53
+ export default class ContentSteeringController
54
+ extends Logger
55
+ implements NetworkComponentAPI
56
+ {
57
+ private readonly hls: Hls;
58
+ private loader: Loader<LoaderContext> | null = null;
59
+ private uri: string | null = null;
60
+ private pathwayId: string = '.';
61
+ private _pathwayPriority: string[] | null = null;
62
+ private timeToLoad: number = 300;
63
+ private reloadTimer: number = -1;
64
+ private updated: number = 0;
65
+ private started: boolean = false;
66
+ private enabled: boolean = true;
67
+ private levels: Level[] | null = null;
68
+ private audioTracks: MediaPlaylist[] | null = null;
69
+ private subtitleTracks: MediaPlaylist[] | null = null;
70
+ private penalizedPathways: { [pathwayId: string]: number } = {};
71
+
72
+ constructor(hls: Hls) {
73
+ super('content-steering', hls.logger);
74
+ this.hls = hls;
75
+ this.registerListeners();
76
+ }
77
+
78
+ private registerListeners() {
79
+ const hls = this.hls;
80
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
81
+ hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
82
+ hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
83
+ hls.on(Events.ERROR, this.onError, this);
84
+ }
85
+
86
+ private unregisterListeners() {
87
+ const hls = this.hls;
88
+ if (!hls) {
89
+ return;
90
+ }
91
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
92
+ hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
93
+ hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
94
+ hls.off(Events.ERROR, this.onError, this);
95
+ }
96
+
97
+ pathways() {
98
+ return (this.levels || []).reduce((pathways, level) => {
99
+ if (pathways.indexOf(level.pathwayId) === -1) {
100
+ pathways.push(level.pathwayId);
101
+ }
102
+ return pathways;
103
+ }, [] as string[]);
104
+ }
105
+
106
+ get pathwayPriority(): string[] | null {
107
+ return this._pathwayPriority;
108
+ }
109
+
110
+ set pathwayPriority(pathwayPriority: string[]) {
111
+ this.updatePathwayPriority(pathwayPriority);
112
+ }
113
+
114
+ startLoad() {
115
+ this.started = true;
116
+ this.clearTimeout();
117
+ if (this.enabled && this.uri) {
118
+ if (this.updated) {
119
+ const ttl = this.timeToLoad * 1000 - (performance.now() - this.updated);
120
+ if (ttl > 0) {
121
+ this.scheduleRefresh(this.uri, ttl);
122
+ return;
123
+ }
124
+ }
125
+ this.loadSteeringManifest(this.uri);
126
+ }
127
+ }
128
+
129
+ stopLoad() {
130
+ this.started = false;
131
+ if (this.loader) {
132
+ this.loader.destroy();
133
+ this.loader = null;
134
+ }
135
+ this.clearTimeout();
136
+ }
137
+
138
+ clearTimeout() {
139
+ if (this.reloadTimer !== -1) {
140
+ self.clearTimeout(this.reloadTimer);
141
+ this.reloadTimer = -1;
142
+ }
143
+ }
144
+
145
+ destroy() {
146
+ this.unregisterListeners();
147
+ this.stopLoad();
148
+ // @ts-ignore
149
+ this.hls = null;
150
+ this.levels = this.audioTracks = this.subtitleTracks = null;
151
+ }
152
+
153
+ removeLevel(levelToRemove: Level) {
154
+ const levels = this.levels;
155
+ if (levels) {
156
+ this.levels = levels.filter((level) => level !== levelToRemove);
157
+ }
158
+ }
159
+
160
+ private onManifestLoading() {
161
+ this.stopLoad();
162
+ this.enabled = true;
163
+ this.timeToLoad = 300;
164
+ this.updated = 0;
165
+ this.uri = null;
166
+ this.pathwayId = '.';
167
+ this.levels = this.audioTracks = this.subtitleTracks = null;
168
+ }
169
+
170
+ private onManifestLoaded(
171
+ event: Events.MANIFEST_LOADED,
172
+ data: ManifestLoadedData,
173
+ ) {
174
+ const { contentSteering } = data;
175
+ if (contentSteering === null) {
176
+ return;
177
+ }
178
+ this.pathwayId = contentSteering.pathwayId;
179
+ this.uri = contentSteering.uri;
180
+ if (this.started) {
181
+ this.startLoad();
182
+ }
183
+ }
184
+
185
+ private onManifestParsed(
186
+ event: Events.MANIFEST_PARSED,
187
+ data: ManifestParsedData,
188
+ ) {
189
+ this.audioTracks = data.audioTracks;
190
+ this.subtitleTracks = data.subtitleTracks;
191
+ }
192
+
193
+ private onError(event: Events.ERROR, data: ErrorData) {
194
+ const { errorAction } = data;
195
+ if (
196
+ errorAction?.action === NetworkErrorAction.SendAlternateToPenaltyBox &&
197
+ errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost
198
+ ) {
199
+ const levels = this.levels;
200
+ let pathwayPriority = this._pathwayPriority;
201
+ let errorPathway = this.pathwayId;
202
+ if (data.context) {
203
+ const { groupId, pathwayId, type } = data.context;
204
+ if (groupId && levels) {
205
+ errorPathway = this.getPathwayForGroupId(groupId, type, errorPathway);
206
+ } else if (pathwayId) {
207
+ errorPathway = pathwayId;
208
+ }
209
+ }
210
+ if (!(errorPathway in this.penalizedPathways)) {
211
+ this.penalizedPathways[errorPathway] = performance.now();
212
+ }
213
+ if (!pathwayPriority && levels) {
214
+ // If PATHWAY-PRIORITY was not provided, list pathways for error handling
215
+ pathwayPriority = this.pathways();
216
+ }
217
+ if (pathwayPriority && pathwayPriority.length > 1) {
218
+ this.updatePathwayPriority(pathwayPriority);
219
+ errorAction.resolved = this.pathwayId !== errorPathway;
220
+ }
221
+ if (data.details === ErrorDetails.BUFFER_APPEND_ERROR && !data.fatal) {
222
+ // Error will become fatal in buffer-controller when reaching `appendErrorMaxRetry`
223
+ // Stream-controllers are expected to reduce buffer length even if this is not deemed a QuotaExceededError
224
+ errorAction.resolved = true;
225
+ } else if (!errorAction.resolved) {
226
+ this.warn(
227
+ `Could not resolve ${data.details} ("${
228
+ data.error.message
229
+ }") with content-steering for Pathway: ${errorPathway} levels: ${
230
+ levels ? levels.length : levels
231
+ } priorities: ${stringify(
232
+ pathwayPriority,
233
+ )} penalized: ${stringify(this.penalizedPathways)}`,
234
+ );
235
+ }
236
+ }
237
+ }
238
+
239
+ public filterParsedLevels(levels: Level[]): Level[] {
240
+ // Filter levels to only include those that are in the initial pathway
241
+ this.levels = levels;
242
+ let pathwayLevels = this.getLevelsForPathway(this.pathwayId);
243
+ if (pathwayLevels.length === 0) {
244
+ const pathwayId = levels[0].pathwayId;
245
+ this.log(
246
+ `No levels found in Pathway ${this.pathwayId}. Setting initial Pathway to "${pathwayId}"`,
247
+ );
248
+ pathwayLevels = this.getLevelsForPathway(pathwayId);
249
+ this.pathwayId = pathwayId;
250
+ }
251
+ if (pathwayLevels.length !== levels.length) {
252
+ this.log(
253
+ `Found ${pathwayLevels.length}/${levels.length} levels in Pathway "${this.pathwayId}"`,
254
+ );
255
+ }
256
+ return pathwayLevels;
257
+ }
258
+
259
+ private getLevelsForPathway(pathwayId: string): Level[] {
260
+ if (this.levels === null) {
261
+ return [];
262
+ }
263
+ return this.levels.filter((level) => pathwayId === level.pathwayId);
264
+ }
265
+
266
+ private updatePathwayPriority(pathwayPriority: string[]) {
267
+ this._pathwayPriority = pathwayPriority;
268
+ let levels: Level[] | undefined;
269
+
270
+ // Evaluate if we should remove the pathway from the penalized list
271
+ const penalizedPathways = this.penalizedPathways;
272
+ const now = performance.now();
273
+ Object.keys(penalizedPathways).forEach((pathwayId) => {
274
+ if (now - penalizedPathways[pathwayId] > PATHWAY_PENALTY_DURATION_MS) {
275
+ delete penalizedPathways[pathwayId];
276
+ }
277
+ });
278
+ for (let i = 0; i < pathwayPriority.length; i++) {
279
+ const pathwayId = pathwayPriority[i];
280
+ if (pathwayId in penalizedPathways) {
281
+ continue;
282
+ }
283
+ if (pathwayId === this.pathwayId) {
284
+ return;
285
+ }
286
+ const selectedIndex = this.hls.nextLoadLevel;
287
+ const selectedLevel: Level = this.hls.levels[selectedIndex];
288
+ levels = this.getLevelsForPathway(pathwayId);
289
+ if (levels.length > 0) {
290
+ this.log(`Setting Pathway to "${pathwayId}"`);
291
+ this.pathwayId = pathwayId;
292
+ reassignFragmentLevelIndexes(levels);
293
+ this.hls.trigger(Events.LEVELS_UPDATED, { levels });
294
+ // Set LevelController's level to trigger LEVEL_SWITCHING which loads playlist if needed
295
+ const levelAfterChange = this.hls.levels[selectedIndex];
296
+ if (selectedLevel && levelAfterChange && this.levels) {
297
+ if (
298
+ levelAfterChange.attrs['STABLE-VARIANT-ID'] !==
299
+ selectedLevel.attrs['STABLE-VARIANT-ID'] &&
300
+ levelAfterChange.bitrate !== selectedLevel.bitrate
301
+ ) {
302
+ this.log(
303
+ `Unstable Pathways change from bitrate ${selectedLevel.bitrate} to ${levelAfterChange.bitrate}`,
304
+ );
305
+ }
306
+ this.hls.nextLoadLevel = selectedIndex;
307
+ }
308
+ break;
309
+ }
310
+ }
311
+ }
312
+
313
+ private getPathwayForGroupId(
314
+ groupId: string,
315
+ type: PlaylistContextType,
316
+ defaultPathway: string,
317
+ ): string {
318
+ const levels = this.getLevelsForPathway(defaultPathway).concat(
319
+ this.levels || [],
320
+ );
321
+ for (let i = 0; i < levels.length; i++) {
322
+ if (
323
+ (type === PlaylistContextType.AUDIO_TRACK &&
324
+ levels[i].hasAudioGroup(groupId)) ||
325
+ (type === PlaylistContextType.SUBTITLE_TRACK &&
326
+ levels[i].hasSubtitleGroup(groupId))
327
+ ) {
328
+ return levels[i].pathwayId;
329
+ }
330
+ }
331
+ return defaultPathway;
332
+ }
333
+
334
+ private clonePathways(pathwayClones: PathwayClone[]) {
335
+ const levels = this.levels;
336
+ if (!levels) {
337
+ return;
338
+ }
339
+ const audioGroupCloneMap: Record<string, string> = {};
340
+ const subtitleGroupCloneMap: Record<string, string> = {};
341
+ pathwayClones.forEach((pathwayClone) => {
342
+ const {
343
+ ID: cloneId,
344
+ 'BASE-ID': baseId,
345
+ 'URI-REPLACEMENT': uriReplacement,
346
+ } = pathwayClone;
347
+ if (levels.some((level) => level.pathwayId === cloneId)) {
348
+ return;
349
+ }
350
+ const clonedVariants = this.getLevelsForPathway(baseId).map(
351
+ (baseLevel) => {
352
+ const attributes = new AttrList(baseLevel.attrs);
353
+ attributes['PATHWAY-ID'] = cloneId;
354
+ const clonedAudioGroupId: string | undefined =
355
+ attributes.AUDIO && `${attributes.AUDIO}_clone_${cloneId}`;
356
+ const clonedSubtitleGroupId: string | undefined =
357
+ attributes.SUBTITLES && `${attributes.SUBTITLES}_clone_${cloneId}`;
358
+ if (clonedAudioGroupId) {
359
+ audioGroupCloneMap[attributes.AUDIO] = clonedAudioGroupId;
360
+ attributes.AUDIO = clonedAudioGroupId;
361
+ }
362
+ if (clonedSubtitleGroupId) {
363
+ subtitleGroupCloneMap[attributes.SUBTITLES] = clonedSubtitleGroupId;
364
+ attributes.SUBTITLES = clonedSubtitleGroupId;
365
+ }
366
+ const url = performUriReplacement(
367
+ baseLevel.uri,
368
+ attributes['STABLE-VARIANT-ID'],
369
+ 'PER-VARIANT-URIS',
370
+ uriReplacement,
371
+ );
372
+ const clonedLevel = new Level({
373
+ attrs: attributes,
374
+ audioCodec: baseLevel.audioCodec,
375
+ bitrate: baseLevel.bitrate,
376
+ height: baseLevel.height,
377
+ name: baseLevel.name,
378
+ url,
379
+ videoCodec: baseLevel.videoCodec,
380
+ width: baseLevel.width,
381
+ });
382
+ if (baseLevel.audioGroups) {
383
+ for (let i = 1; i < baseLevel.audioGroups.length; i++) {
384
+ clonedLevel.addGroupId(
385
+ 'audio',
386
+ `${baseLevel.audioGroups[i]}_clone_${cloneId}`,
387
+ );
388
+ }
389
+ }
390
+ if (baseLevel.subtitleGroups) {
391
+ for (let i = 1; i < baseLevel.subtitleGroups.length; i++) {
392
+ clonedLevel.addGroupId(
393
+ 'text',
394
+ `${baseLevel.subtitleGroups[i]}_clone_${cloneId}`,
395
+ );
396
+ }
397
+ }
398
+ return clonedLevel;
399
+ },
400
+ );
401
+ levels.push(...clonedVariants);
402
+ cloneRenditionGroups(
403
+ this.audioTracks,
404
+ audioGroupCloneMap,
405
+ uriReplacement,
406
+ cloneId,
407
+ );
408
+ cloneRenditionGroups(
409
+ this.subtitleTracks,
410
+ subtitleGroupCloneMap,
411
+ uriReplacement,
412
+ cloneId,
413
+ );
414
+ });
415
+ }
416
+
417
+ private loadSteeringManifest(uri: string) {
418
+ const config = this.hls.config;
419
+ const Loader = config.loader;
420
+ if (this.loader) {
421
+ this.loader.destroy();
422
+ }
423
+ this.loader = new Loader(config) as Loader<LoaderContext>;
424
+
425
+ let url: URL;
426
+ try {
427
+ url = new self.URL(uri);
428
+ } catch (error) {
429
+ this.enabled = false;
430
+ this.log(`Failed to parse Steering Manifest URI: ${uri}`);
431
+ return;
432
+ }
433
+ if (url.protocol !== 'data:') {
434
+ const throughput =
435
+ (this.hls.bandwidthEstimate || config.abrEwmaDefaultEstimate) | 0;
436
+ url.searchParams.set('_HLS_pathway', this.pathwayId);
437
+ url.searchParams.set('_HLS_throughput', '' + throughput);
438
+ }
439
+ const context: LoaderContext = {
440
+ responseType: 'json',
441
+ url: url.href,
442
+ };
443
+
444
+ const loadPolicy = config.steeringManifestLoadPolicy.default;
445
+ const legacyRetryCompatibility: RetryConfig | Record<string, void> =
446
+ loadPolicy.errorRetry || loadPolicy.timeoutRetry || {};
447
+ const loaderConfig: LoaderConfiguration = {
448
+ loadPolicy,
449
+ timeout: loadPolicy.maxLoadTimeMs,
450
+ maxRetry: legacyRetryCompatibility.maxNumRetry || 0,
451
+ retryDelay: legacyRetryCompatibility.retryDelayMs || 0,
452
+ maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0,
453
+ };
454
+
455
+ const callbacks: LoaderCallbacks<LoaderContext> = {
456
+ onSuccess: (
457
+ response: LoaderResponse,
458
+ stats: LoaderStats,
459
+ context: LoaderContext,
460
+ networkDetails: NullableNetworkDetails,
461
+ ) => {
462
+ this.log(`Loaded steering manifest: "${url}"`);
463
+ const steeringData = response.data as SteeringManifest;
464
+ if (steeringData?.VERSION !== 1) {
465
+ this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
466
+ return;
467
+ }
468
+ this.updated = performance.now();
469
+ this.timeToLoad = steeringData.TTL;
470
+ const {
471
+ 'RELOAD-URI': reloadUri,
472
+ 'PATHWAY-CLONES': pathwayClones,
473
+ 'PATHWAY-PRIORITY': pathwayPriority,
474
+ } = steeringData;
475
+ if (reloadUri) {
476
+ try {
477
+ this.uri = new self.URL(reloadUri, url).href;
478
+ } catch (error) {
479
+ this.enabled = false;
480
+ this.log(
481
+ `Failed to parse Steering Manifest RELOAD-URI: ${reloadUri}`,
482
+ );
483
+ return;
484
+ }
485
+ }
486
+ this.scheduleRefresh(this.uri || context.url);
487
+ if (pathwayClones) {
488
+ this.clonePathways(pathwayClones);
489
+ }
490
+
491
+ const loadedSteeringData: SteeringManifestLoadedData = {
492
+ steeringManifest: steeringData,
493
+ url: url.toString(),
494
+ };
495
+ this.hls.trigger(Events.STEERING_MANIFEST_LOADED, loadedSteeringData);
496
+
497
+ if (pathwayPriority) {
498
+ this.updatePathwayPriority(pathwayPriority);
499
+ }
500
+ },
501
+
502
+ onError: (
503
+ error: { code: number; text: string },
504
+ context: LoaderContext,
505
+ networkDetails: NullableNetworkDetails,
506
+ stats: LoaderStats,
507
+ ) => {
508
+ this.log(
509
+ `Error loading steering manifest: ${error.code} ${error.text} (${context.url})`,
510
+ );
511
+ this.stopLoad();
512
+ if (error.code === 410) {
513
+ this.enabled = false;
514
+ this.log(`Steering manifest ${context.url} no longer available`);
515
+ return;
516
+ }
517
+ let ttl = this.timeToLoad * 1000;
518
+ if (error.code === 429) {
519
+ const loader = this.loader;
520
+ if (typeof loader?.getResponseHeader === 'function') {
521
+ const retryAfter = loader.getResponseHeader('Retry-After');
522
+ if (retryAfter) {
523
+ ttl = parseFloat(retryAfter) * 1000;
524
+ }
525
+ }
526
+ this.log(`Steering manifest ${context.url} rate limited`);
527
+ return;
528
+ }
529
+ this.scheduleRefresh(this.uri || context.url, ttl);
530
+ },
531
+
532
+ onTimeout: (
533
+ stats: LoaderStats,
534
+ context: LoaderContext,
535
+ networkDetails: NullableNetworkDetails,
536
+ ) => {
537
+ this.log(`Timeout loading steering manifest (${context.url})`);
538
+ this.scheduleRefresh(this.uri || context.url);
539
+ },
540
+ };
541
+
542
+ this.log(`Requesting steering manifest: ${url}`);
543
+ this.loader.load(context, loaderConfig, callbacks);
544
+ }
545
+
546
+ private scheduleRefresh(uri: string, ttlMs: number = this.timeToLoad * 1000) {
547
+ this.clearTimeout();
548
+ this.reloadTimer = self.setTimeout(() => {
549
+ const media = this.hls?.media;
550
+ if (media && !media.ended) {
551
+ this.loadSteeringManifest(uri);
552
+ return;
553
+ }
554
+ this.scheduleRefresh(uri, this.timeToLoad * 1000);
555
+ }, ttlMs);
556
+ }
557
+ }
558
+
559
+ function cloneRenditionGroups(
560
+ tracks: MediaPlaylist[] | null,
561
+ groupCloneMap: Record<string, string>,
562
+ uriReplacement: UriReplacement,
563
+ cloneId: string,
564
+ ) {
565
+ if (!tracks) {
566
+ return;
567
+ }
568
+ Object.keys(groupCloneMap).forEach((audioGroupId) => {
569
+ const clonedTracks = tracks
570
+ .filter((track) => track.groupId === audioGroupId)
571
+ .map((track) => {
572
+ const clonedTrack = Object.assign({}, track);
573
+ clonedTrack.details = undefined;
574
+ clonedTrack.attrs = new AttrList(clonedTrack.attrs) as MediaAttributes;
575
+ clonedTrack.url = clonedTrack.attrs.URI = performUriReplacement(
576
+ track.url,
577
+ track.attrs['STABLE-RENDITION-ID'],
578
+ 'PER-RENDITION-URIS',
579
+ uriReplacement,
580
+ );
581
+ clonedTrack.groupId = clonedTrack.attrs['GROUP-ID'] =
582
+ groupCloneMap[audioGroupId];
583
+ clonedTrack.attrs['PATHWAY-ID'] = cloneId;
584
+ return clonedTrack;
585
+ });
586
+ tracks.push(...clonedTracks);
587
+ });
588
+ }
589
+
590
+ function performUriReplacement(
591
+ uri: string,
592
+ stableId: string | undefined,
593
+ perOptionKey: 'PER-VARIANT-URIS' | 'PER-RENDITION-URIS',
594
+ uriReplacement: UriReplacement,
595
+ ): string {
596
+ const {
597
+ HOST: host,
598
+ PARAMS: params,
599
+ [perOptionKey]: perOptionUris,
600
+ } = uriReplacement;
601
+ let perVariantUri;
602
+ if (stableId) {
603
+ perVariantUri = perOptionUris?.[stableId];
604
+ if (perVariantUri) {
605
+ uri = perVariantUri;
606
+ }
607
+ }
608
+ const url = new self.URL(uri);
609
+ if (host && !perVariantUri) {
610
+ url.hostname = host;
611
+ }
612
+ if (params) {
613
+ Object.keys(params)
614
+ .sort()
615
+ .forEach((key) => {
616
+ if (key) {
617
+ url.searchParams.set(key, params[key]);
618
+ }
619
+ });
620
+ }
621
+ return url.href;
622
+ }