@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,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
|
+
}
|