@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.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- 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
|
+
}
|