@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,627 @@
1
+ import { findFragmentByPTS } from './fragment-finders';
2
+ import { ErrorDetails, ErrorTypes } from '../errors';
3
+ import { Events } from '../events';
4
+ import { HdcpLevels } from '../types/level';
5
+ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
6
+ import { getCodecsForMimeType } from '../utils/codecs';
7
+ import {
8
+ getRetryConfig,
9
+ isKeyError,
10
+ isTimeoutError,
11
+ isUnusableKeyError,
12
+ shouldRetry,
13
+ } from '../utils/error-helper';
14
+ import { arrayToHex } from '../utils/hex';
15
+ import { Logger } from '../utils/logger';
16
+ import type { RetryConfig } from '../config';
17
+ import type { LevelKey } from '../hls';
18
+ import type Hls from '../hls';
19
+ import type { Fragment, MediaFragment } from '../loader/fragment';
20
+ import type { NetworkComponentAPI } from '../types/component-api';
21
+ import type { ErrorData } from '../types/events';
22
+ import type { HdcpLevel, Level } from '../types/level';
23
+
24
+ export const enum NetworkErrorAction {
25
+ DoNothing = 0,
26
+ SendEndCallback = 1,
27
+ SendAlternateToPenaltyBox = 2,
28
+ RemoveAlternatePermanently = 3,
29
+ InsertDiscontinuity = 4,
30
+ RetryRequest = 5,
31
+ }
32
+
33
+ export const enum ErrorActionFlags {
34
+ None = 0,
35
+ MoveAllAlternatesMatchingHost = 1,
36
+ MoveAllAlternatesMatchingHDCP = 2,
37
+ MoveAllAlternatesMatchingKey = 4,
38
+ SwitchToSDR = 8,
39
+ ResetMediaSource = 16,
40
+ }
41
+
42
+ export type IErrorAction = {
43
+ action: NetworkErrorAction;
44
+ flags: ErrorActionFlags;
45
+ retryCount?: number;
46
+ retryConfig?: RetryConfig;
47
+ hdcpLevel?: HdcpLevel;
48
+ nextAutoLevel?: number;
49
+ resolved?: boolean;
50
+ };
51
+ export default class ErrorController
52
+ extends Logger
53
+ implements NetworkComponentAPI
54
+ {
55
+ private readonly hls: Hls;
56
+ private playlistError: number = 0;
57
+
58
+ constructor(hls: Hls) {
59
+ super('error-controller', hls.logger);
60
+ this.hls = hls;
61
+ this.registerListeners();
62
+ }
63
+
64
+ private registerListeners() {
65
+ const hls = this.hls;
66
+ hls.on(Events.ERROR, this.onError, this);
67
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
68
+ hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
69
+ }
70
+
71
+ private unregisterListeners() {
72
+ const hls = this.hls;
73
+ if (!hls) {
74
+ return;
75
+ }
76
+ hls.off(Events.ERROR, this.onError, this);
77
+ hls.off(Events.ERROR, this.onErrorOut, this);
78
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
79
+ hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
80
+ }
81
+
82
+ destroy() {
83
+ this.unregisterListeners();
84
+ // @ts-ignore
85
+ this.hls = null;
86
+ }
87
+
88
+ startLoad(startPosition: number): void {}
89
+
90
+ stopLoad(): void {
91
+ this.playlistError = 0;
92
+ }
93
+
94
+ private getVariantLevelIndex(frag: Fragment | undefined): number {
95
+ if (frag?.type === PlaylistLevelType.MAIN) {
96
+ return frag.level;
97
+ }
98
+ return this.getVariantIndex();
99
+ }
100
+
101
+ private getVariantIndex(): number {
102
+ const hls = this.hls;
103
+ const currentLevel = hls.currentLevel;
104
+ if (hls.loadLevelObj?.details || currentLevel === -1) {
105
+ return hls.loadLevel;
106
+ }
107
+ return currentLevel;
108
+ }
109
+
110
+ private variantHasKey(
111
+ level: Level | undefined,
112
+ keyInError: LevelKey,
113
+ ): boolean {
114
+ if (level) {
115
+ if (level.details?.hasKey(keyInError)) {
116
+ return true;
117
+ }
118
+ const audioGroupsIds = level.audioGroups;
119
+ if (audioGroupsIds) {
120
+ const audioTracks = this.hls.allAudioTracks.filter(
121
+ (track) => audioGroupsIds.indexOf(track.groupId) >= 0,
122
+ );
123
+ return audioTracks.some((track) => track.details?.hasKey(keyInError));
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+
129
+ private onManifestLoading() {
130
+ this.playlistError = 0;
131
+ }
132
+
133
+ private onLevelUpdated() {
134
+ this.playlistError = 0;
135
+ }
136
+
137
+ private onError(event: Events.ERROR, data: ErrorData) {
138
+ if (data.fatal) {
139
+ return;
140
+ }
141
+ const hls = this.hls;
142
+ const context = data.context;
143
+
144
+ switch (data.details) {
145
+ case ErrorDetails.FRAG_LOAD_ERROR:
146
+ case ErrorDetails.FRAG_LOAD_TIMEOUT:
147
+ case ErrorDetails.KEY_LOAD_ERROR:
148
+ case ErrorDetails.KEY_LOAD_TIMEOUT:
149
+ data.errorAction = this.getFragRetryOrSwitchAction(data);
150
+ return;
151
+ case ErrorDetails.FRAG_PARSING_ERROR:
152
+ // ignore empty segment errors marked as gap
153
+ if (data.frag?.gap) {
154
+ data.errorAction = createDoNothingErrorAction();
155
+ return;
156
+ }
157
+ // falls through
158
+ case ErrorDetails.FRAG_GAP:
159
+ case ErrorDetails.FRAG_DECRYPT_ERROR: {
160
+ // Switch level if possible, otherwise allow retry count to reach max error retries
161
+ data.errorAction = this.getFragRetryOrSwitchAction(data);
162
+ data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox;
163
+ return;
164
+ }
165
+ case ErrorDetails.PLAYLIST_UNCHANGED_ERROR:
166
+ case ErrorDetails.LEVEL_EMPTY_ERROR:
167
+ case ErrorDetails.LEVEL_PARSING_ERROR:
168
+ {
169
+ // Only retry when empty and live
170
+ const levelIndex =
171
+ data.parent === PlaylistLevelType.MAIN
172
+ ? (data.level as number)
173
+ : hls.loadLevel;
174
+ if (
175
+ data.details === ErrorDetails.LEVEL_EMPTY_ERROR &&
176
+ !!data.context?.levelDetails?.live
177
+ ) {
178
+ data.errorAction = this.getPlaylistRetryOrSwitchAction(
179
+ data,
180
+ levelIndex,
181
+ );
182
+ } else {
183
+ // Escalate to fatal if not retrying or switching
184
+ data.levelRetry = false;
185
+ data.errorAction = this.getLevelSwitchAction(data, levelIndex);
186
+ }
187
+ }
188
+ return;
189
+ case ErrorDetails.LEVEL_LOAD_ERROR:
190
+ case ErrorDetails.LEVEL_LOAD_TIMEOUT:
191
+ if (typeof context?.level === 'number') {
192
+ data.errorAction = this.getPlaylistRetryOrSwitchAction(
193
+ data,
194
+ context.level,
195
+ );
196
+ }
197
+ return;
198
+ case ErrorDetails.AUDIO_TRACK_LOAD_ERROR:
199
+ case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT:
200
+ case ErrorDetails.SUBTITLE_LOAD_ERROR:
201
+ case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT:
202
+ if (context) {
203
+ const level = hls.loadLevelObj;
204
+ if (
205
+ level &&
206
+ ((context.type === PlaylistContextType.AUDIO_TRACK &&
207
+ level.hasAudioGroup(context.groupId)) ||
208
+ (context.type === PlaylistContextType.SUBTITLE_TRACK &&
209
+ level.hasSubtitleGroup(context.groupId)))
210
+ ) {
211
+ // Perform Pathway switch or Redundant failover if possible for fastest recovery
212
+ // otherwise allow playlist retry count to reach max error retries
213
+ data.errorAction = this.getPlaylistRetryOrSwitchAction(
214
+ data,
215
+ hls.loadLevel,
216
+ );
217
+ data.errorAction.action =
218
+ NetworkErrorAction.SendAlternateToPenaltyBox;
219
+ data.errorAction.flags =
220
+ ErrorActionFlags.MoveAllAlternatesMatchingHost;
221
+ return;
222
+ }
223
+ }
224
+ return;
225
+ case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:
226
+ {
227
+ data.errorAction = {
228
+ action: NetworkErrorAction.SendAlternateToPenaltyBox,
229
+ flags: ErrorActionFlags.MoveAllAlternatesMatchingHDCP,
230
+ };
231
+ }
232
+ return;
233
+ case ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED:
234
+ case ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR:
235
+ case ErrorDetails.KEY_SYSTEM_NO_SESSION:
236
+ {
237
+ data.errorAction = {
238
+ action: NetworkErrorAction.SendAlternateToPenaltyBox,
239
+ flags: ErrorActionFlags.MoveAllAlternatesMatchingKey,
240
+ };
241
+ }
242
+ return;
243
+ case ErrorDetails.BUFFER_ADD_CODEC_ERROR:
244
+ case ErrorDetails.REMUX_ALLOC_ERROR:
245
+ case ErrorDetails.BUFFER_APPEND_ERROR:
246
+ // Buffer-controller can set errorAction when append errors can be ignored or resolved locally
247
+ if (!data.errorAction) {
248
+ data.errorAction = this.getLevelSwitchAction(data, data.level);
249
+ }
250
+ return;
251
+ case ErrorDetails.MEDIA_SOURCE_REQUIRES_RESET:
252
+ if (!data.errorAction) {
253
+ data.errorAction = this.getLevelSwitchAction(data, data.level);
254
+ }
255
+ data.errorAction.flags |=
256
+ ErrorActionFlags.ResetMediaSource | ErrorActionFlags.SwitchToSDR;
257
+ return;
258
+ case ErrorDetails.INTERNAL_EXCEPTION:
259
+ case ErrorDetails.BUFFER_APPENDING_ERROR:
260
+ case ErrorDetails.BUFFER_FULL_ERROR:
261
+ case ErrorDetails.LEVEL_SWITCH_ERROR:
262
+ case ErrorDetails.BUFFER_STALLED_ERROR:
263
+ case ErrorDetails.BUFFER_SEEK_OVER_HOLE:
264
+ case ErrorDetails.BUFFER_NUDGE_ON_STALL:
265
+ data.errorAction = createDoNothingErrorAction();
266
+ return;
267
+ }
268
+
269
+ if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
270
+ // Do not retry level. Should be fatal if ErrorDetails.KEY_SYSTEM_<ERROR> not handled with early return above.
271
+ data.levelRetry = false;
272
+ data.errorAction = createDoNothingErrorAction();
273
+ }
274
+ }
275
+
276
+ private getPlaylistRetryOrSwitchAction(
277
+ data: ErrorData,
278
+ levelIndex: number | null | undefined,
279
+ ): IErrorAction {
280
+ const hls = this.hls;
281
+ const retryConfig = getRetryConfig(hls.config.playlistLoadPolicy, data);
282
+ const retryCount = this.playlistError++;
283
+ const retry = shouldRetry(
284
+ retryConfig,
285
+ retryCount,
286
+ isTimeoutError(data),
287
+ data.response,
288
+ );
289
+ if (retry) {
290
+ return {
291
+ action: NetworkErrorAction.RetryRequest,
292
+ flags: ErrorActionFlags.None,
293
+ retryConfig,
294
+ retryCount,
295
+ };
296
+ }
297
+ const errorAction = this.getLevelSwitchAction(data, levelIndex);
298
+ if (retryConfig) {
299
+ errorAction.retryConfig = retryConfig;
300
+ errorAction.retryCount = retryCount;
301
+ }
302
+ return errorAction;
303
+ }
304
+
305
+ private getFragRetryOrSwitchAction(data: ErrorData): IErrorAction {
306
+ const hls = this.hls;
307
+ // Share fragment error count accross media options (main, audio, subs)
308
+ // This allows for level based rendition switching when media option assets fail
309
+ const variantLevelIndex = this.getVariantLevelIndex(data.frag);
310
+ const level = hls.levels[variantLevelIndex];
311
+ const { fragLoadPolicy, keyLoadPolicy } = hls.config;
312
+ const retryConfig = getRetryConfig(
313
+ isKeyError(data) ? keyLoadPolicy : fragLoadPolicy,
314
+ data,
315
+ );
316
+ const fragmentErrors = hls.levels.reduce(
317
+ (acc, level) => acc + level.fragmentError,
318
+ 0,
319
+ );
320
+ // Switch levels when out of retried or level index out of bounds
321
+ if (level) {
322
+ if (data.details !== ErrorDetails.FRAG_GAP) {
323
+ level.fragmentError++;
324
+ }
325
+ if (!isUnusableKeyError(data)) {
326
+ const retry = shouldRetry(
327
+ retryConfig,
328
+ fragmentErrors,
329
+ isTimeoutError(data),
330
+ data.response,
331
+ );
332
+ if (retry) {
333
+ return {
334
+ action: NetworkErrorAction.RetryRequest,
335
+ flags: ErrorActionFlags.None,
336
+ retryConfig,
337
+ retryCount: fragmentErrors,
338
+ };
339
+ }
340
+ }
341
+ }
342
+ // Reach max retry count, or Missing level reference
343
+ // Switch to valid index
344
+ const errorAction = this.getLevelSwitchAction(data, variantLevelIndex);
345
+ // Add retry details to allow skipping of FRAG_PARSING_ERROR
346
+ if (retryConfig) {
347
+ errorAction.retryConfig = retryConfig;
348
+ errorAction.retryCount = fragmentErrors;
349
+ }
350
+ return errorAction;
351
+ }
352
+
353
+ private getLevelSwitchAction(
354
+ data: ErrorData,
355
+ levelIndex: number | null | undefined,
356
+ ): IErrorAction {
357
+ const hls = this.hls;
358
+ if (levelIndex === null || levelIndex === undefined) {
359
+ levelIndex = hls.loadLevel;
360
+ }
361
+ const level = this.hls.levels[levelIndex];
362
+ if (level) {
363
+ const errorDetails = data.details;
364
+ level.loadError++;
365
+ if (errorDetails === ErrorDetails.BUFFER_APPEND_ERROR) {
366
+ level.fragmentError++;
367
+ }
368
+ // Search for next level to retry
369
+ let nextLevel = -1;
370
+ const { levels, loadLevel, minAutoLevel, maxAutoLevel } = hls;
371
+ if (!hls.autoLevelEnabled && !hls.config.preserveManualLevelOnError) {
372
+ hls.loadLevel = -1;
373
+ }
374
+ const fragErrorType = data.frag?.type;
375
+ // Find alternate audio codec if available on audio codec error
376
+ const isAudioCodecError =
377
+ (fragErrorType === PlaylistLevelType.AUDIO &&
378
+ errorDetails === ErrorDetails.FRAG_PARSING_ERROR) ||
379
+ (data.sourceBufferName === 'audio' && isCodecRelated(errorDetails));
380
+ const findAudioCodecAlternate =
381
+ isAudioCodecError &&
382
+ levels.some(({ audioCodec }) => level.audioCodec !== audioCodec);
383
+ // Find alternate video codec if available on video codec error
384
+ const isVideoCodecError =
385
+ data.sourceBufferName === 'video' && isCodecRelated(errorDetails);
386
+ const findVideoCodecAlternate =
387
+ isVideoCodecError &&
388
+ levels.some(
389
+ ({ codecSet, audioCodec }) =>
390
+ level.codecSet !== codecSet && level.audioCodec === audioCodec,
391
+ );
392
+ const { type: playlistErrorType, groupId: playlistErrorGroupId } =
393
+ data.context ?? {};
394
+ for (let i = levels.length; i--; ) {
395
+ const candidate = (i + loadLevel) % levels.length;
396
+ if (
397
+ candidate !== loadLevel &&
398
+ candidate >= minAutoLevel &&
399
+ candidate <= maxAutoLevel &&
400
+ levels[candidate].loadError === 0
401
+ ) {
402
+ const levelCandidate = levels[candidate];
403
+
404
+ // Skip level switch if GAP tag is found in next level at same position
405
+ if (
406
+ errorDetails === ErrorDetails.FRAG_GAP &&
407
+ fragErrorType === PlaylistLevelType.MAIN &&
408
+ data.frag
409
+ ) {
410
+ const levelDetails = levels[candidate].details;
411
+ if (levelDetails) {
412
+ const fragCandidate = findFragmentByPTS(
413
+ data.frag as MediaFragment,
414
+ levelDetails.fragments,
415
+ data.frag.start,
416
+ );
417
+ if (fragCandidate?.gap) {
418
+ continue;
419
+ }
420
+ }
421
+ } else if (
422
+ (playlistErrorType === PlaylistContextType.AUDIO_TRACK &&
423
+ levelCandidate.hasAudioGroup(playlistErrorGroupId)) ||
424
+ (playlistErrorType === PlaylistContextType.SUBTITLE_TRACK &&
425
+ levelCandidate.hasSubtitleGroup(playlistErrorGroupId))
426
+ ) {
427
+ // For audio/subs playlist errors find another group ID or fallthrough to redundant fail-over
428
+ continue;
429
+ } else if (
430
+ (fragErrorType === PlaylistLevelType.AUDIO &&
431
+ level.audioGroups?.some((groupId) =>
432
+ levelCandidate.hasAudioGroup(groupId),
433
+ )) ||
434
+ (fragErrorType === PlaylistLevelType.SUBTITLE &&
435
+ level.subtitleGroups?.some((groupId) =>
436
+ levelCandidate.hasSubtitleGroup(groupId),
437
+ )) ||
438
+ (findAudioCodecAlternate &&
439
+ level.audioCodec === levelCandidate.audioCodec) ||
440
+ (findVideoCodecAlternate &&
441
+ level.codecSet === levelCandidate.codecSet) ||
442
+ (!findAudioCodecAlternate &&
443
+ level.audioCodec !== levelCandidate.audioCodec)
444
+ ) {
445
+ // For video/audio/subs frag errors find another group ID or fallthrough to redundant fail-over
446
+ continue;
447
+ }
448
+ nextLevel = candidate;
449
+ break;
450
+ }
451
+ }
452
+ if (nextLevel > -1 && hls.loadLevel !== nextLevel) {
453
+ data.levelRetry = true;
454
+ this.playlistError = 0;
455
+ return {
456
+ action: NetworkErrorAction.SendAlternateToPenaltyBox,
457
+ flags: ErrorActionFlags.None,
458
+ nextAutoLevel: nextLevel,
459
+ };
460
+ }
461
+ }
462
+ // No levels to switch / Manual level selection / Level not found
463
+ // Resolve with Pathway switch, Redundant fail-over, or stay on lowest Level
464
+ return {
465
+ action: NetworkErrorAction.SendAlternateToPenaltyBox,
466
+ flags: ErrorActionFlags.MoveAllAlternatesMatchingHost,
467
+ };
468
+ }
469
+
470
+ public onErrorOut(event: Events.ERROR, data: ErrorData) {
471
+ switch (data.errorAction?.action) {
472
+ case NetworkErrorAction.DoNothing:
473
+ break;
474
+ case NetworkErrorAction.SendAlternateToPenaltyBox:
475
+ this.sendAlternateToPenaltyBox(data);
476
+ if (
477
+ !data.errorAction.resolved &&
478
+ data.details !== ErrorDetails.FRAG_GAP
479
+ ) {
480
+ data.fatal = true;
481
+ }
482
+ break;
483
+ case NetworkErrorAction.RetryRequest:
484
+ // handled by stream and playlist/level controllers
485
+ break;
486
+ }
487
+
488
+ const flags = data.errorAction?.flags || 0;
489
+ if (flags & ErrorActionFlags.ResetMediaSource) {
490
+ this.hls.recoverMediaError();
491
+ }
492
+
493
+ if (data.fatal) {
494
+ this.hls.stopLoad();
495
+ return;
496
+ }
497
+ }
498
+
499
+ private sendAlternateToPenaltyBox(data: ErrorData) {
500
+ const hls = this.hls;
501
+ const errorAction = data.errorAction;
502
+ if (!errorAction) {
503
+ return;
504
+ }
505
+ let nextAutoLevel = errorAction.nextAutoLevel;
506
+
507
+ if (errorAction.flags === ErrorActionFlags.None) {
508
+ this.switchLevel(data, nextAutoLevel);
509
+ } else if (errorAction.flags & ErrorActionFlags.SwitchToSDR) {
510
+ // Penalize all levels with current video-range
511
+ const levels = this.hls.levels;
512
+ const levelCountWithError = levels.length;
513
+ for (let i = levelCountWithError; i--; ) {
514
+ if (levels[i].videoRange !== 'SDR') {
515
+ levels[i].fragmentError++;
516
+ levels[i].loadError++;
517
+ } else if (nextAutoLevel === undefined) {
518
+ nextAutoLevel = i;
519
+ }
520
+ }
521
+ } else if (
522
+ errorAction.flags & ErrorActionFlags.MoveAllAlternatesMatchingHDCP
523
+ ) {
524
+ const levelIndex = this.getVariantLevelIndex(data.frag);
525
+ const level = hls.levels[levelIndex];
526
+ const restrictedHdcpLevel = (level as Level | undefined)?.attrs[
527
+ 'HDCP-LEVEL'
528
+ ];
529
+ errorAction.hdcpLevel = restrictedHdcpLevel;
530
+ const restrictedOutputErrorWhileNoneDeclared =
531
+ restrictedHdcpLevel === 'NONE';
532
+ if (restrictedHdcpLevel && !restrictedOutputErrorWhileNoneDeclared) {
533
+ hls.maxHdcpLevel =
534
+ HdcpLevels[HdcpLevels.indexOf(restrictedHdcpLevel) - 1];
535
+ errorAction.resolved = true;
536
+ this.warn(
537
+ `Restricting playback to HDCP-LEVEL of "${hls.maxHdcpLevel}" or lower`,
538
+ );
539
+ } else {
540
+ if (restrictedOutputErrorWhileNoneDeclared) {
541
+ this.warn(`HDCP policy resticted output with HDCP-LEVEL=NONE`);
542
+ }
543
+ // Move alternates matching key when no HDCP-LEVEL attribute is found
544
+ errorAction.flags |= ErrorActionFlags.MoveAllAlternatesMatchingKey;
545
+ }
546
+ }
547
+ if (errorAction.flags & ErrorActionFlags.MoveAllAlternatesMatchingKey) {
548
+ const levelKey = data.decryptdata;
549
+ if (levelKey) {
550
+ // Penalize all levels with key
551
+ const levels = this.hls.levels;
552
+ const levelCountWithError = levels.length;
553
+ for (let i = levelCountWithError; i--; ) {
554
+ if (this.variantHasKey(levels[i], levelKey)) {
555
+ this.log(
556
+ `Banned key found in level ${i} (${levels[i].bitrate}bps) or audio group "${levels[i].audioGroups?.join(',')}" (${data.frag?.type} fragment) ${arrayToHex(levelKey.keyId || [])}`,
557
+ );
558
+ levels[i].fragmentError++;
559
+ levels[i].loadError++;
560
+ this.log(`Removing level ${i} with key error (${data.error})`);
561
+ this.hls.removeLevel(i);
562
+ }
563
+ }
564
+ const frag = data.frag;
565
+ if (this.hls.levels.length < levelCountWithError) {
566
+ errorAction.resolved = true;
567
+ } else if (frag && frag.type !== PlaylistLevelType.MAIN) {
568
+ // Ignore key error for audio track with unmatched key (main session error)
569
+ const fragLevelKey = frag.decryptdata;
570
+ if (fragLevelKey && !levelKey.matches(fragLevelKey)) {
571
+ errorAction.resolved = true;
572
+ }
573
+ }
574
+ }
575
+ }
576
+
577
+ // If not resolved by previous actions try to switch to next level
578
+ if (!errorAction.resolved) {
579
+ this.switchLevel(data, nextAutoLevel);
580
+ }
581
+ }
582
+
583
+ private switchLevel(data: ErrorData, levelIndex: number | undefined) {
584
+ if (levelIndex !== undefined && data.errorAction) {
585
+ this.warn(`switching to level ${levelIndex} after ${data.details}`);
586
+ this.hls.nextAutoLevel = levelIndex;
587
+ data.errorAction.resolved = true;
588
+ // Stream controller is responsible for this but won't switch on false start
589
+ this.hls.nextLoadLevel = this.hls.nextAutoLevel;
590
+ if (
591
+ data.details === ErrorDetails.BUFFER_ADD_CODEC_ERROR &&
592
+ data.mimeType &&
593
+ data.sourceBufferName !== 'audiovideo'
594
+ ) {
595
+ const codec = getCodecsForMimeType(data.mimeType);
596
+ const levels = this.hls.levels;
597
+ for (let i = levels.length; i--; ) {
598
+ if (levels[i][`${data.sourceBufferName}Codec`] === codec) {
599
+ this.log(
600
+ `Removing level ${i} for ${data.details} ("${codec}" not supported)`,
601
+ );
602
+ this.hls.removeLevel(i);
603
+ }
604
+ }
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ function isCodecRelated(errorDetails: ErrorDetails): boolean {
611
+ return (
612
+ errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR ||
613
+ errorDetails === ErrorDetails.BUFFER_APPEND_ERROR ||
614
+ errorDetails === ErrorDetails.MEDIA_SOURCE_REQUIRES_RESET
615
+ );
616
+ }
617
+
618
+ export function createDoNothingErrorAction(resolved?: boolean): IErrorAction {
619
+ const errorAction: IErrorAction = {
620
+ action: NetworkErrorAction.DoNothing,
621
+ flags: ErrorActionFlags.None,
622
+ };
623
+ if (resolved) {
624
+ errorAction.resolved = true;
625
+ }
626
+ return errorAction;
627
+ }