@codenkay/video-nsgplayer-core 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/index.cjs +1288 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +547 -0
- package/dist/index.d.ts +547 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
// src/errors/PlayerError.ts
|
|
2
|
+
var PlayerErrorCode = /* @__PURE__ */ ((PlayerErrorCode2) => {
|
|
3
|
+
PlayerErrorCode2["INVALID_CONFIGURATION"] = "INVALID_CONFIGURATION";
|
|
4
|
+
PlayerErrorCode2["AUTHENTICATION_FAILED"] = "AUTHENTICATION_FAILED";
|
|
5
|
+
PlayerErrorCode2["AUTH_TOKEN_REFRESH_FAILED"] = "AUTH_TOKEN_REFRESH_FAILED";
|
|
6
|
+
PlayerErrorCode2["TOKEN_EXPIRED"] = "TOKEN_EXPIRED";
|
|
7
|
+
PlayerErrorCode2["TOKEN_REFRESH_FAILED"] = "TOKEN_REFRESH_FAILED";
|
|
8
|
+
PlayerErrorCode2["SOURCE_RESOLUTION_FAILED"] = "SOURCE_RESOLUTION_FAILED";
|
|
9
|
+
PlayerErrorCode2["MANIFEST_LOAD_FAILED"] = "MANIFEST_LOAD_FAILED";
|
|
10
|
+
PlayerErrorCode2["SEGMENT_LOAD_FAILED"] = "SEGMENT_LOAD_FAILED";
|
|
11
|
+
PlayerErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
12
|
+
PlayerErrorCode2["PLAYBACK_ERROR"] = "PLAYBACK_ERROR";
|
|
13
|
+
PlayerErrorCode2["UNSUPPORTED_BROWSER"] = "UNSUPPORTED_BROWSER";
|
|
14
|
+
PlayerErrorCode2["AUTOPLAY_BLOCKED"] = "AUTOPLAY_BLOCKED";
|
|
15
|
+
PlayerErrorCode2["UNKNOWN"] = "UNKNOWN";
|
|
16
|
+
return PlayerErrorCode2;
|
|
17
|
+
})(PlayerErrorCode || {});
|
|
18
|
+
function createPlayerError(code, message, options) {
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
message: redactSensitive(message),
|
|
22
|
+
category: options?.category ?? "unknown",
|
|
23
|
+
fatal: options?.fatal ?? true,
|
|
24
|
+
retryable: options?.retryable ?? false,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
cause: options?.cause
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function redactSensitive(input) {
|
|
30
|
+
return input.replace(/(Bearer\s+)[^\s]+/gi, "$1[REDACTED]").replace(/([?&](token|expires|signature|sig)=)[^&]+/gi, "$1[REDACTED]").replace(/(Authorization:\s*)[^\s]+/gi, "$1[REDACTED]");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/auth/AuthManager.ts
|
|
34
|
+
var AuthManager = class {
|
|
35
|
+
constructor(auth, refreshBeforeExpiryMs, logger, onRefreshed, onError, maxUnauthorizedRetries = 1) {
|
|
36
|
+
this.auth = auth;
|
|
37
|
+
this.refreshBeforeExpiryMs = refreshBeforeExpiryMs;
|
|
38
|
+
this.logger = logger;
|
|
39
|
+
this.onRefreshed = onRefreshed;
|
|
40
|
+
this.onError = onError;
|
|
41
|
+
this.token = null;
|
|
42
|
+
this.inFlight = null;
|
|
43
|
+
this.timer = null;
|
|
44
|
+
this.destroyed = false;
|
|
45
|
+
/** Consecutive unauthorized refresh attempts (reset on success). */
|
|
46
|
+
this.unauthorizedAttempts = 0;
|
|
47
|
+
this.maxUnauthorizedRetries = Math.max(0, maxUnauthorizedRetries);
|
|
48
|
+
}
|
|
49
|
+
getAccessToken() {
|
|
50
|
+
return this.token?.accessToken ?? null;
|
|
51
|
+
}
|
|
52
|
+
getExpiresAt() {
|
|
53
|
+
return this.token?.expiresAt ?? null;
|
|
54
|
+
}
|
|
55
|
+
async ensureToken(context) {
|
|
56
|
+
if (!this.auth?.getToken) return null;
|
|
57
|
+
if (this.destroyed) return null;
|
|
58
|
+
if (this.token && context.reason !== "unauthorized" && context.reason !== "manual" && this.token.expiresAt - Date.now() > this.refreshBeforeExpiryMs) {
|
|
59
|
+
return this.token;
|
|
60
|
+
}
|
|
61
|
+
return this.refresh(context);
|
|
62
|
+
}
|
|
63
|
+
async refresh(context) {
|
|
64
|
+
if (!this.auth?.getToken) {
|
|
65
|
+
throw createPlayerError(
|
|
66
|
+
"INVALID_CONFIGURATION" /* INVALID_CONFIGURATION */,
|
|
67
|
+
"auth.getToken is required for authenticated playback",
|
|
68
|
+
{ category: "config", fatal: true, retryable: false }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (context.reason === "unauthorized") {
|
|
72
|
+
if (this.unauthorizedAttempts >= this.maxUnauthorizedRetries) {
|
|
73
|
+
const message = "Auth token refresh aborted: exceeded bounded unauthorized retry limit";
|
|
74
|
+
this.onError?.(message, false);
|
|
75
|
+
throw createPlayerError(
|
|
76
|
+
"AUTH_TOKEN_REFRESH_FAILED" /* AUTH_TOKEN_REFRESH_FAILED */,
|
|
77
|
+
message,
|
|
78
|
+
{ category: "auth", fatal: true, retryable: false }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
this.unauthorizedAttempts += 1;
|
|
82
|
+
}
|
|
83
|
+
if (this.inFlight) return this.inFlight;
|
|
84
|
+
this.inFlight = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const next = await this.auth.getToken(context);
|
|
87
|
+
if (!next?.accessToken || typeof next.expiresAt !== "number" || next.expiresAt <= 0) {
|
|
88
|
+
throw createPlayerError(
|
|
89
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
90
|
+
"auth.getToken returned an invalid token payload",
|
|
91
|
+
{ category: "auth", fatal: true, retryable: true }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
this.token = next;
|
|
95
|
+
if (context.reason !== "unauthorized") {
|
|
96
|
+
this.unauthorizedAttempts = 0;
|
|
97
|
+
}
|
|
98
|
+
this.scheduleProactiveRefresh();
|
|
99
|
+
this.onRefreshed?.(next);
|
|
100
|
+
this.logger.debug("Auth token refreshed", {
|
|
101
|
+
reason: context.reason,
|
|
102
|
+
expiresAt: next.expiresAt
|
|
103
|
+
});
|
|
104
|
+
return next;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error && typeof error === "object" && "code" in error && error.code === "AUTH_TOKEN_REFRESH_FAILED" /* AUTH_TOKEN_REFRESH_FAILED */) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const message = error instanceof Error ? error.message : "Auth token refresh failed";
|
|
110
|
+
const retryable = this.unauthorizedAttempts <= this.maxUnauthorizedRetries;
|
|
111
|
+
this.onError?.(message, retryable);
|
|
112
|
+
throw createPlayerError(
|
|
113
|
+
"AUTH_TOKEN_REFRESH_FAILED" /* AUTH_TOKEN_REFRESH_FAILED */,
|
|
114
|
+
message,
|
|
115
|
+
{ category: "auth", fatal: !retryable, retryable, cause: error }
|
|
116
|
+
);
|
|
117
|
+
} finally {
|
|
118
|
+
this.inFlight = null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
return this.inFlight;
|
|
122
|
+
}
|
|
123
|
+
scheduleProactiveRefresh() {
|
|
124
|
+
if (this.timer) clearTimeout(this.timer);
|
|
125
|
+
if (!this.token || this.destroyed) return;
|
|
126
|
+
const delay = Math.max(
|
|
127
|
+
5e3,
|
|
128
|
+
this.token.expiresAt - Date.now() - this.refreshBeforeExpiryMs
|
|
129
|
+
);
|
|
130
|
+
this.timer = setTimeout(() => {
|
|
131
|
+
void this.refresh({
|
|
132
|
+
reason: "expiring",
|
|
133
|
+
videoId: void 0
|
|
134
|
+
}).catch(() => {
|
|
135
|
+
});
|
|
136
|
+
}, delay);
|
|
137
|
+
}
|
|
138
|
+
destroy() {
|
|
139
|
+
this.destroyed = true;
|
|
140
|
+
if (this.timer) clearTimeout(this.timer);
|
|
141
|
+
this.timer = null;
|
|
142
|
+
this.token = null;
|
|
143
|
+
this.inFlight = null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/config/defaults.ts
|
|
148
|
+
var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
149
|
+
var PACKAGE_VERSION = "0.1.0";
|
|
150
|
+
var DEFAULT_PLAYER_BEHAVIOR = {
|
|
151
|
+
autoplay: true,
|
|
152
|
+
startMuted: true,
|
|
153
|
+
volume: 1,
|
|
154
|
+
startPosition: 0,
|
|
155
|
+
skipSeconds: 10,
|
|
156
|
+
speedOptionCount: PLAYBACK_RATES.length,
|
|
157
|
+
zoomEnabled: true,
|
|
158
|
+
defaultZoom: 1,
|
|
159
|
+
playbackRates: PLAYBACK_RATES
|
|
160
|
+
};
|
|
161
|
+
var DEFAULT_CONTROLS = {
|
|
162
|
+
enabled: true,
|
|
163
|
+
showQuality: true,
|
|
164
|
+
showSpeed: true,
|
|
165
|
+
showCaptions: true,
|
|
166
|
+
showAudio: true,
|
|
167
|
+
showVolume: true,
|
|
168
|
+
showFullscreen: true,
|
|
169
|
+
showZoom: true,
|
|
170
|
+
showSkip: true,
|
|
171
|
+
showLive: true,
|
|
172
|
+
seekCueDurationMs: 720,
|
|
173
|
+
keyboardShortcuts: true
|
|
174
|
+
};
|
|
175
|
+
var DEFAULT_THEME = {
|
|
176
|
+
playIconPreset: "triangle",
|
|
177
|
+
playButtonBackground: "#ffffff",
|
|
178
|
+
playButtonIconColor: "#ffffff",
|
|
179
|
+
seekTrackColor: "#334155",
|
|
180
|
+
seekProgressColor: "#38bdf8",
|
|
181
|
+
seekBufferedColor: "#64748b",
|
|
182
|
+
timeColor: "#e2e8f0",
|
|
183
|
+
controlIconColor: "#f1f5f9",
|
|
184
|
+
focusRingColor: "#38bdf8",
|
|
185
|
+
spinnerColor: "#38bdf8",
|
|
186
|
+
volumeIconColor: "#f1f5f9",
|
|
187
|
+
volumeTrackColor: "#334155",
|
|
188
|
+
volumeProgressColor: "#38bdf8",
|
|
189
|
+
qualityOptionTextColor: "#e2e8f0",
|
|
190
|
+
qualityOptionBackground: "#020617",
|
|
191
|
+
qualityOptionActiveBackground: "#0c4a6e",
|
|
192
|
+
qualityOptionActiveTextColor: "#bae6fd",
|
|
193
|
+
speedOptionTextColor: "#e2e8f0",
|
|
194
|
+
speedOptionBackground: "#020617",
|
|
195
|
+
speedOptionActiveBackground: "#0c4a6e",
|
|
196
|
+
speedOptionActiveTextColor: "#bae6fd",
|
|
197
|
+
zoomIconColor: "#f1f5f9",
|
|
198
|
+
skipIconColor: "#f1f5f9",
|
|
199
|
+
skipCueBackground: "#ffffff",
|
|
200
|
+
skipCueTextColor: "#ffffff",
|
|
201
|
+
liveDotColor: "#ef4444"
|
|
202
|
+
};
|
|
203
|
+
var DEFAULT_WATERMARK = {
|
|
204
|
+
enabled: true,
|
|
205
|
+
text: "",
|
|
206
|
+
durationMs: 1e4,
|
|
207
|
+
intervalMaxMs: 3e5
|
|
208
|
+
};
|
|
209
|
+
var DEFAULT_PLAYER_CONFIG = {
|
|
210
|
+
player: DEFAULT_PLAYER_BEHAVIOR,
|
|
211
|
+
controls: DEFAULT_CONTROLS,
|
|
212
|
+
theme: DEFAULT_THEME,
|
|
213
|
+
watermark: DEFAULT_WATERMARK,
|
|
214
|
+
streaming: {
|
|
215
|
+
engine: "hls",
|
|
216
|
+
hls: {
|
|
217
|
+
enableWorker: true,
|
|
218
|
+
lowLatencyMode: true,
|
|
219
|
+
backBufferLength: 90,
|
|
220
|
+
fatalRecoveryLimit: 3
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
logging: { enabled: false, level: "error" },
|
|
224
|
+
debug: { enabled: false, network: false, events: false, state: false },
|
|
225
|
+
accessibility: { reducedMotion: false }
|
|
226
|
+
};
|
|
227
|
+
var FORBIDDEN_SECRET_KEYS = [
|
|
228
|
+
"clientsecret",
|
|
229
|
+
"apisecret",
|
|
230
|
+
"privatekey",
|
|
231
|
+
"video_api_secret",
|
|
232
|
+
"basicauthpassword",
|
|
233
|
+
"password"
|
|
234
|
+
];
|
|
235
|
+
function assertNoSecretsInConfig(raw) {
|
|
236
|
+
if (!raw || typeof raw !== "object") return;
|
|
237
|
+
const walk = (obj, path) => {
|
|
238
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
239
|
+
const lower = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
240
|
+
if (FORBIDDEN_SECRET_KEYS.some((k) => lower.includes(k))) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`INVALID_CONFIGURATION: Forbidden secret key "${path}${key}" must never be passed to the browser SDK. Keep clientSecret on your server/BFF and use auth.getToken().`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
246
|
+
walk(value, `${path}${key}.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
walk(raw, "");
|
|
251
|
+
}
|
|
252
|
+
function mergePlayerConfig(partial) {
|
|
253
|
+
assertNoSecretsInConfig(partial);
|
|
254
|
+
return {
|
|
255
|
+
player: { ...DEFAULT_PLAYER_BEHAVIOR, ...partial?.player },
|
|
256
|
+
controls: { ...DEFAULT_CONTROLS, ...partial?.controls },
|
|
257
|
+
theme: { ...DEFAULT_THEME, ...partial?.theme },
|
|
258
|
+
watermark: { ...DEFAULT_WATERMARK, ...partial?.watermark },
|
|
259
|
+
streaming: {
|
|
260
|
+
engine: "hls",
|
|
261
|
+
hls: {
|
|
262
|
+
...DEFAULT_PLAYER_CONFIG.streaming.hls,
|
|
263
|
+
...partial?.streaming?.hls
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
|
|
267
|
+
debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
|
|
268
|
+
accessibility: {
|
|
269
|
+
...DEFAULT_PLAYER_CONFIG.accessibility,
|
|
270
|
+
...partial?.accessibility
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function themeToCssVars(theme) {
|
|
275
|
+
return {
|
|
276
|
+
"--player-play-btn-bg": theme.playButtonBackground,
|
|
277
|
+
"--player-play-btn-icon": theme.playButtonIconColor,
|
|
278
|
+
"--player-seek-track": theme.seekTrackColor,
|
|
279
|
+
"--player-seek-progress": theme.seekProgressColor,
|
|
280
|
+
"--player-seek-buffered": theme.seekBufferedColor,
|
|
281
|
+
"--player-time": theme.timeColor,
|
|
282
|
+
"--player-control-icon": theme.controlIconColor,
|
|
283
|
+
"--player-focus-ring": theme.focusRingColor,
|
|
284
|
+
"--player-spinner": theme.spinnerColor,
|
|
285
|
+
"--player-volume-icon": theme.volumeIconColor,
|
|
286
|
+
"--player-volume-track": theme.volumeTrackColor,
|
|
287
|
+
"--player-volume-progress": theme.volumeProgressColor,
|
|
288
|
+
"--player-quality-text": theme.qualityOptionTextColor,
|
|
289
|
+
"--player-quality-bg": theme.qualityOptionBackground,
|
|
290
|
+
"--player-quality-active-bg": theme.qualityOptionActiveBackground,
|
|
291
|
+
"--player-quality-active-text": theme.qualityOptionActiveTextColor,
|
|
292
|
+
"--player-speed-text": theme.speedOptionTextColor,
|
|
293
|
+
"--player-speed-bg": theme.speedOptionBackground,
|
|
294
|
+
"--player-speed-active-bg": theme.speedOptionActiveBackground,
|
|
295
|
+
"--player-speed-active-text": theme.speedOptionActiveTextColor,
|
|
296
|
+
"--player-zoom-icon": theme.zoomIconColor,
|
|
297
|
+
"--player-skip-icon": theme.skipIconColor,
|
|
298
|
+
"--player-skip-cue-bg": theme.skipCueBackground,
|
|
299
|
+
"--player-skip-cue-text": theme.skipCueTextColor,
|
|
300
|
+
"--player-live-dot": theme.liveDotColor
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/events/EventEmitter.ts
|
|
305
|
+
var EventEmitter = class {
|
|
306
|
+
constructor() {
|
|
307
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
308
|
+
}
|
|
309
|
+
on(event, handler) {
|
|
310
|
+
let set = this.listeners.get(event);
|
|
311
|
+
if (!set) {
|
|
312
|
+
set = /* @__PURE__ */ new Set();
|
|
313
|
+
this.listeners.set(event, set);
|
|
314
|
+
}
|
|
315
|
+
set.add(handler);
|
|
316
|
+
return () => this.off(event, handler);
|
|
317
|
+
}
|
|
318
|
+
off(event, handler) {
|
|
319
|
+
this.listeners.get(event)?.delete(handler);
|
|
320
|
+
}
|
|
321
|
+
emit(event, ...args) {
|
|
322
|
+
const set = this.listeners.get(event);
|
|
323
|
+
if (!set) return;
|
|
324
|
+
for (const handler of set) {
|
|
325
|
+
try {
|
|
326
|
+
handler(...args);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
removeAll() {
|
|
332
|
+
this.listeners.clear();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// src/security/redaction.ts
|
|
337
|
+
var SENSITIVE_KEYS = /^(authorization|cookie|set-cookie|x-api-key|token|accessToken|access_token|clientSecret|password)$/i;
|
|
338
|
+
function redactValue(key, value) {
|
|
339
|
+
if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
|
|
340
|
+
if (typeof value === "string") {
|
|
341
|
+
if (/^https?:\/\//i.test(value) && /[?&](token|expires)=/i.test(value)) {
|
|
342
|
+
try {
|
|
343
|
+
const url = new URL(value);
|
|
344
|
+
for (const param of ["token", "expires", "signature", "sig"]) {
|
|
345
|
+
if (url.searchParams.has(param)) {
|
|
346
|
+
url.searchParams.set(param, "[REDACTED]");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return url.toString();
|
|
350
|
+
} catch {
|
|
351
|
+
return "[REDACTED_URL]";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
356
|
+
return redactObject(value);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
function redactObject(obj) {
|
|
361
|
+
const out = {};
|
|
362
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
363
|
+
out[key] = redactValue(key, value);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/logging/Logger.ts
|
|
369
|
+
var LEVELS = ["silent", "error", "warn", "info", "debug", "trace"];
|
|
370
|
+
var Logger = class {
|
|
371
|
+
constructor(config) {
|
|
372
|
+
this.config = config;
|
|
373
|
+
}
|
|
374
|
+
update(config) {
|
|
375
|
+
this.config = { ...this.config, ...config };
|
|
376
|
+
}
|
|
377
|
+
shouldLog(level) {
|
|
378
|
+
if (!this.config.enabled || this.config.level === "silent") return false;
|
|
379
|
+
return LEVELS.indexOf(level) <= LEVELS.indexOf(this.config.level);
|
|
380
|
+
}
|
|
381
|
+
write(level, message, details) {
|
|
382
|
+
if (!this.shouldLog(level)) return;
|
|
383
|
+
const payload = details ? redactObject(details) : void 0;
|
|
384
|
+
const fn = console[level] ?? console.log;
|
|
385
|
+
if (payload) fn(`[nsg-player] ${message}`, payload);
|
|
386
|
+
else fn(`[nsg-player] ${message}`);
|
|
387
|
+
}
|
|
388
|
+
error(message, details) {
|
|
389
|
+
this.write("error", message, details);
|
|
390
|
+
}
|
|
391
|
+
warn(message, details) {
|
|
392
|
+
this.write("warn", message, details);
|
|
393
|
+
}
|
|
394
|
+
info(message, details) {
|
|
395
|
+
this.write("info", message, details);
|
|
396
|
+
}
|
|
397
|
+
debug(message, details) {
|
|
398
|
+
this.write("debug", message, details);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/auth/AuthParamsStore.ts
|
|
403
|
+
var AuthParamsStore = class {
|
|
404
|
+
constructor(initial) {
|
|
405
|
+
if (initial instanceof URLSearchParams) {
|
|
406
|
+
this.params = new URLSearchParams(initial);
|
|
407
|
+
} else if (initial) {
|
|
408
|
+
this.params = new URLSearchParams(initial);
|
|
409
|
+
} else {
|
|
410
|
+
this.params = new URLSearchParams();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
getSnapshot() {
|
|
414
|
+
return new URLSearchParams(this.params);
|
|
415
|
+
}
|
|
416
|
+
toRecord() {
|
|
417
|
+
const out = {};
|
|
418
|
+
this.params.forEach((value, key) => {
|
|
419
|
+
out[key] = value;
|
|
420
|
+
});
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
update(next) {
|
|
424
|
+
this.params = next instanceof URLSearchParams ? new URLSearchParams(next) : new URLSearchParams(next);
|
|
425
|
+
}
|
|
426
|
+
applyToUrl(rawUrl, baseOrigin) {
|
|
427
|
+
const target = new URL(rawUrl, baseOrigin ?? (typeof location !== "undefined" ? location.origin : "http://localhost"));
|
|
428
|
+
this.params.forEach((value, key) => {
|
|
429
|
+
target.searchParams.set(key, value);
|
|
430
|
+
});
|
|
431
|
+
return target.toString();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// src/playback/PlaybackSourceManager.ts
|
|
436
|
+
var PlaybackSourceManager = class {
|
|
437
|
+
constructor(playback, logger) {
|
|
438
|
+
this.playback = playback;
|
|
439
|
+
this.logger = logger;
|
|
440
|
+
this.source = null;
|
|
441
|
+
this.inFlight = null;
|
|
442
|
+
this.authParams = new AuthParamsStore();
|
|
443
|
+
}
|
|
444
|
+
getSource() {
|
|
445
|
+
return this.source;
|
|
446
|
+
}
|
|
447
|
+
getExpiresAt() {
|
|
448
|
+
return this.source?.expiresAt ?? null;
|
|
449
|
+
}
|
|
450
|
+
async load(context) {
|
|
451
|
+
if (!this.playback?.getSource) {
|
|
452
|
+
throw createPlayerError(
|
|
453
|
+
"INVALID_CONFIGURATION" /* INVALID_CONFIGURATION */,
|
|
454
|
+
"playback.getSource is required when not using sourceUrl",
|
|
455
|
+
{ category: "config", fatal: true, retryable: false }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return this.run(context, () => this.playback.getSource(context));
|
|
459
|
+
}
|
|
460
|
+
async refresh(context) {
|
|
461
|
+
const refresher = this.playback?.refreshSource ?? this.playback?.getSource;
|
|
462
|
+
if (!refresher) {
|
|
463
|
+
throw createPlayerError(
|
|
464
|
+
"INVALID_CONFIGURATION" /* INVALID_CONFIGURATION */,
|
|
465
|
+
"playback.refreshSource or playback.getSource is required",
|
|
466
|
+
{ category: "config", fatal: true, retryable: false }
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return this.run(context, () => refresher(context));
|
|
470
|
+
}
|
|
471
|
+
applyDirectSource(source) {
|
|
472
|
+
this.source = source;
|
|
473
|
+
this.applyAuthParams(source);
|
|
474
|
+
}
|
|
475
|
+
async run(context, fn) {
|
|
476
|
+
if (this.inFlight) return this.inFlight;
|
|
477
|
+
this.inFlight = (async () => {
|
|
478
|
+
try {
|
|
479
|
+
const next = await fn();
|
|
480
|
+
if (!next?.url || typeof next.expiresAt !== "number") {
|
|
481
|
+
throw createPlayerError(
|
|
482
|
+
"SOURCE_RESOLUTION_FAILED" /* SOURCE_RESOLUTION_FAILED */,
|
|
483
|
+
"Playback source callback returned an invalid payload",
|
|
484
|
+
{ category: "playback", fatal: true, retryable: true }
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
this.source = next;
|
|
488
|
+
this.applyAuthParams(next);
|
|
489
|
+
this.logger.debug("Playback source resolved", {
|
|
490
|
+
reason: context.reason,
|
|
491
|
+
expiresAt: next.expiresAt
|
|
492
|
+
});
|
|
493
|
+
return next;
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (error && typeof error === "object" && "code" in error) throw error;
|
|
496
|
+
throw createPlayerError(
|
|
497
|
+
"SOURCE_RESOLUTION_FAILED" /* SOURCE_RESOLUTION_FAILED */,
|
|
498
|
+
error instanceof Error ? error.message : "Source resolution failed",
|
|
499
|
+
{ category: "playback", fatal: true, retryable: true, cause: error }
|
|
500
|
+
);
|
|
501
|
+
} finally {
|
|
502
|
+
this.inFlight = null;
|
|
503
|
+
}
|
|
504
|
+
})();
|
|
505
|
+
return this.inFlight;
|
|
506
|
+
}
|
|
507
|
+
applyAuthParams(source) {
|
|
508
|
+
if (source.authParams) {
|
|
509
|
+
this.authParams.update(source.authParams);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const url = new URL(source.url);
|
|
514
|
+
const params = {};
|
|
515
|
+
url.searchParams.forEach((value, key) => {
|
|
516
|
+
params[key] = value;
|
|
517
|
+
});
|
|
518
|
+
if (source.token) params.token = source.token;
|
|
519
|
+
this.authParams.update(params);
|
|
520
|
+
} catch {
|
|
521
|
+
if (source.token) {
|
|
522
|
+
this.authParams.update({ token: source.token });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
destroy() {
|
|
527
|
+
this.source = null;
|
|
528
|
+
this.inFlight = null;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// src/playback/RefreshManager.ts
|
|
533
|
+
var RefreshManager = class {
|
|
534
|
+
constructor(options) {
|
|
535
|
+
this.options = options;
|
|
536
|
+
this.timer = null;
|
|
537
|
+
this.coalesceTimer = null;
|
|
538
|
+
this.inFlight = false;
|
|
539
|
+
this.consecutiveFailures = 0;
|
|
540
|
+
this.stopped = false;
|
|
541
|
+
this.pendingReason = null;
|
|
542
|
+
}
|
|
543
|
+
start(initialExpiresAt) {
|
|
544
|
+
this.schedule(initialExpiresAt);
|
|
545
|
+
}
|
|
546
|
+
requestRenew(reason = "frag_error") {
|
|
547
|
+
if (this.stopped || this.options.isDestroyed()) return;
|
|
548
|
+
this.pendingReason = reason;
|
|
549
|
+
if (this.coalesceTimer) return;
|
|
550
|
+
this.coalesceTimer = setTimeout(() => {
|
|
551
|
+
this.coalesceTimer = null;
|
|
552
|
+
const reasonToUse = this.pendingReason ?? "frag_error";
|
|
553
|
+
this.pendingReason = null;
|
|
554
|
+
void this.renew(reasonToUse);
|
|
555
|
+
}, this.options.coalesceMs);
|
|
556
|
+
}
|
|
557
|
+
stop() {
|
|
558
|
+
this.stopped = true;
|
|
559
|
+
if (this.timer) clearTimeout(this.timer);
|
|
560
|
+
if (this.coalesceTimer) clearTimeout(this.coalesceTimer);
|
|
561
|
+
this.timer = null;
|
|
562
|
+
this.coalesceTimer = null;
|
|
563
|
+
}
|
|
564
|
+
schedule(expiresAt) {
|
|
565
|
+
if (this.timer) clearTimeout(this.timer);
|
|
566
|
+
if (this.stopped) return;
|
|
567
|
+
const delay = Math.max(
|
|
568
|
+
5e3,
|
|
569
|
+
expiresAt - Date.now() - this.options.refreshBeforeExpiryMs
|
|
570
|
+
);
|
|
571
|
+
this.timer = setTimeout(() => {
|
|
572
|
+
void this.renew("proactive");
|
|
573
|
+
}, delay);
|
|
574
|
+
}
|
|
575
|
+
async renew(reason) {
|
|
576
|
+
if (this.stopped || this.options.isDestroyed() || this.inFlight) {
|
|
577
|
+
if (this.inFlight) this.pendingReason = reason;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
this.inFlight = true;
|
|
581
|
+
this.options.onRenewing?.();
|
|
582
|
+
try {
|
|
583
|
+
await this.options.authManager.ensureToken({
|
|
584
|
+
reason: reason === "proactive" ? "expiring" : "unauthorized",
|
|
585
|
+
videoId: this.options.videoId
|
|
586
|
+
});
|
|
587
|
+
const source = await this.options.sourceManager.refresh({
|
|
588
|
+
videoId: this.options.videoId,
|
|
589
|
+
reason: reason === "proactive" ? "expiring" : reason === "manual" ? "manual" : "auth_error"
|
|
590
|
+
});
|
|
591
|
+
this.consecutiveFailures = 0;
|
|
592
|
+
this.options.onRenewed?.(source.expiresAt);
|
|
593
|
+
this.schedule(source.expiresAt);
|
|
594
|
+
this.options.logger.debug("Playback source renewed", { reason });
|
|
595
|
+
} catch (error) {
|
|
596
|
+
this.consecutiveFailures += 1;
|
|
597
|
+
const message = error instanceof Error ? error.message : "Playback refresh failed";
|
|
598
|
+
this.options.logger.warn("Playback refresh failed", {
|
|
599
|
+
reason,
|
|
600
|
+
failure: this.consecutiveFailures
|
|
601
|
+
});
|
|
602
|
+
if (this.consecutiveFailures >= this.options.renewMaxRetries) {
|
|
603
|
+
this.options.onFatal?.(message);
|
|
604
|
+
} else {
|
|
605
|
+
const backoff = this.options.renewBackoffMs * this.consecutiveFailures;
|
|
606
|
+
this.timer = setTimeout(() => {
|
|
607
|
+
void this.renew(reason);
|
|
608
|
+
}, backoff);
|
|
609
|
+
}
|
|
610
|
+
} finally {
|
|
611
|
+
this.inFlight = false;
|
|
612
|
+
if (this.pendingReason && !this.stopped) {
|
|
613
|
+
const pending = this.pendingReason;
|
|
614
|
+
this.pendingReason = null;
|
|
615
|
+
void this.renew(pending);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/playback/liveEdge.ts
|
|
622
|
+
var DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS = 3;
|
|
623
|
+
function getLiveEdgeTime(media) {
|
|
624
|
+
const { seekable, duration } = media;
|
|
625
|
+
try {
|
|
626
|
+
if (seekable.length > 0) {
|
|
627
|
+
const end = seekable.end(seekable.length - 1);
|
|
628
|
+
if (Number.isFinite(end) && end >= 0) {
|
|
629
|
+
return end;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
635
|
+
return duration;
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
function isAtLiveEdge(media, toleranceSeconds = DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS) {
|
|
640
|
+
const edge = getLiveEdgeTime(media);
|
|
641
|
+
if (edge === null) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
const lag = edge - media.currentTime;
|
|
645
|
+
return lag <= Math.max(0, toleranceSeconds);
|
|
646
|
+
}
|
|
647
|
+
function seekToLiveEdge(media, options) {
|
|
648
|
+
const tolerance = options?.toleranceSeconds ?? DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS;
|
|
649
|
+
const edgeOffset = options?.edgeOffsetSeconds ?? 0.25;
|
|
650
|
+
const edge = getLiveEdgeTime(media);
|
|
651
|
+
if (edge === null) {
|
|
652
|
+
return { moved: false, reason: "edge_unavailable", edge: null };
|
|
653
|
+
}
|
|
654
|
+
if (isAtLiveEdge(media, tolerance)) {
|
|
655
|
+
return { moved: false, reason: "already_at_live", edge };
|
|
656
|
+
}
|
|
657
|
+
const target = Math.max(0, edge - edgeOffset);
|
|
658
|
+
media.currentTime = target;
|
|
659
|
+
return { moved: true, edge };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/streaming/HlsEngine.ts
|
|
663
|
+
var HlsEngine = class {
|
|
664
|
+
constructor(options) {
|
|
665
|
+
this.options = options;
|
|
666
|
+
this.hls = null;
|
|
667
|
+
this.destroyed = false;
|
|
668
|
+
this.supportMode = "unsupported";
|
|
669
|
+
this.networkRecoveryCount = 0;
|
|
670
|
+
this.mediaRecoveryCount = 0;
|
|
671
|
+
this.qualities = [];
|
|
672
|
+
this.qualitySelection = "auto";
|
|
673
|
+
}
|
|
674
|
+
async start() {
|
|
675
|
+
const { videoEl, playbackUrl, autoplay, startMuted, volume, startPosition } = this.options;
|
|
676
|
+
videoEl.muted = startMuted;
|
|
677
|
+
if (typeof volume === "number") videoEl.volume = Math.min(1, Math.max(0, volume));
|
|
678
|
+
const Hls = (await import("hls.js")).default;
|
|
679
|
+
if (Hls.isSupported()) {
|
|
680
|
+
this.supportMode = "hls.js";
|
|
681
|
+
this.options.callbacks.onSupportMode?.("hls.js");
|
|
682
|
+
await this.startWithHlsJs(Hls, playbackUrl);
|
|
683
|
+
} else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) {
|
|
684
|
+
this.supportMode = "native";
|
|
685
|
+
this.options.callbacks.onSupportMode?.("native");
|
|
686
|
+
videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
|
|
687
|
+
this.bindMediaEvents();
|
|
688
|
+
this.options.callbacks.onStateChange?.("ready");
|
|
689
|
+
} else {
|
|
690
|
+
this.supportMode = "unsupported";
|
|
691
|
+
this.options.callbacks.onSupportMode?.("unsupported");
|
|
692
|
+
this.options.callbacks.onFatalError?.("HLS is not supported in this browser");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (typeof startPosition === "number" && startPosition > 0) {
|
|
696
|
+
videoEl.currentTime = startPosition;
|
|
697
|
+
}
|
|
698
|
+
if (autoplay) {
|
|
699
|
+
try {
|
|
700
|
+
await videoEl.play();
|
|
701
|
+
} catch {
|
|
702
|
+
if (!videoEl.muted) {
|
|
703
|
+
videoEl.muted = true;
|
|
704
|
+
try {
|
|
705
|
+
await videoEl.play();
|
|
706
|
+
} catch {
|
|
707
|
+
this.options.logger.warn("Autoplay blocked");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async startWithHlsJs(Hls, playbackUrl) {
|
|
714
|
+
const { videoEl, authParams, hlsConfig, callbacks } = this.options;
|
|
715
|
+
this.hls = new Hls({
|
|
716
|
+
enableWorker: hlsConfig.enableWorker,
|
|
717
|
+
lowLatencyMode: hlsConfig.lowLatencyMode,
|
|
718
|
+
backBufferLength: hlsConfig.backBufferLength,
|
|
719
|
+
xhrSetup: (xhr, url) => {
|
|
720
|
+
void xhr;
|
|
721
|
+
void url;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
this.hls.attachMedia(videoEl);
|
|
725
|
+
this.hls.loadSource(authParams.applyToUrl(playbackUrl));
|
|
726
|
+
this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
|
|
727
|
+
this.qualities = (data.levels ?? []).map((level, index) => ({
|
|
728
|
+
index,
|
|
729
|
+
height: level.height ?? 0,
|
|
730
|
+
width: level.width ?? 0,
|
|
731
|
+
bitrate: level.bitrate ?? 0,
|
|
732
|
+
label: level.height != null && level.height > 0 ? `${level.height}p` : `Level ${index}`
|
|
733
|
+
}));
|
|
734
|
+
callbacks.onQualitiesChange?.(this.qualities);
|
|
735
|
+
callbacks.onStateChange?.("ready");
|
|
736
|
+
});
|
|
737
|
+
this.hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
738
|
+
if (!data.fatal) {
|
|
739
|
+
if (data.type === Hls.ErrorTypes.NETWORK_ERROR && (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR || data.details === Hls.ErrorDetails.KEY_LOAD_ERROR)) {
|
|
740
|
+
callbacks.onAuthRecoveryNeeded?.(
|
|
741
|
+
data.details === Hls.ErrorDetails.KEY_LOAD_ERROR ? "key_error" : data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR ? "level_error" : "frag_error"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
|
747
|
+
this.networkRecoveryCount += 1;
|
|
748
|
+
if (this.networkRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
|
|
749
|
+
callbacks.onAuthRecoveryNeeded?.("fatal_network");
|
|
750
|
+
this.hls?.startLoad();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
755
|
+
this.mediaRecoveryCount += 1;
|
|
756
|
+
if (this.mediaRecoveryCount <= hlsConfig.fatalRecoveryLimit) {
|
|
757
|
+
this.hls?.recoverMediaError();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
callbacks.onFatalError?.(data.details || "Fatal HLS error");
|
|
762
|
+
});
|
|
763
|
+
this.bindMediaEvents();
|
|
764
|
+
}
|
|
765
|
+
bindMediaEvents() {
|
|
766
|
+
const { videoEl, callbacks } = this.options;
|
|
767
|
+
videoEl.addEventListener("playing", () => callbacks.onStateChange?.("playing"));
|
|
768
|
+
videoEl.addEventListener("pause", () => {
|
|
769
|
+
if (!videoEl.ended) callbacks.onStateChange?.("paused");
|
|
770
|
+
});
|
|
771
|
+
videoEl.addEventListener("waiting", () => callbacks.onStateChange?.("buffering"));
|
|
772
|
+
videoEl.addEventListener("ended", () => {
|
|
773
|
+
callbacks.onStateChange?.("ended");
|
|
774
|
+
callbacks.onEnded?.();
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
updateAuthParams() {
|
|
778
|
+
if (this.supportMode === "native" && this.options.videoEl) {
|
|
779
|
+
const time = this.options.videoEl.currentTime;
|
|
780
|
+
const wasPaused = this.options.videoEl.paused;
|
|
781
|
+
this.options.videoEl.src = this.options.authParams.applyToUrl(
|
|
782
|
+
this.options.playbackUrl
|
|
783
|
+
);
|
|
784
|
+
this.options.videoEl.currentTime = time;
|
|
785
|
+
if (!wasPaused) void this.options.videoEl.play();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
resumeAfterAuthRenew() {
|
|
789
|
+
this.hls?.startLoad();
|
|
790
|
+
this.updateAuthParams();
|
|
791
|
+
}
|
|
792
|
+
setQuality(selection) {
|
|
793
|
+
this.qualitySelection = selection;
|
|
794
|
+
if (!this.hls) return;
|
|
795
|
+
this.hls.currentLevel = selection === "auto" ? -1 : selection;
|
|
796
|
+
this.options.callbacks.onQualitySelectionChange?.(selection);
|
|
797
|
+
}
|
|
798
|
+
getQuality() {
|
|
799
|
+
return this.qualitySelection;
|
|
800
|
+
}
|
|
801
|
+
getQualities() {
|
|
802
|
+
return this.qualities;
|
|
803
|
+
}
|
|
804
|
+
getSupportMode() {
|
|
805
|
+
return this.supportMode;
|
|
806
|
+
}
|
|
807
|
+
destroy() {
|
|
808
|
+
if (this.destroyed) return;
|
|
809
|
+
this.destroyed = true;
|
|
810
|
+
this.hls?.destroy();
|
|
811
|
+
this.hls = null;
|
|
812
|
+
const { videoEl } = this.options;
|
|
813
|
+
videoEl.removeAttribute("src");
|
|
814
|
+
videoEl.load();
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/player/createVideoPlayer.ts
|
|
819
|
+
function expiryStatus(expiresAt, safetyMs) {
|
|
820
|
+
if (expiresAt === null || expiresAt <= 0) return "unknown";
|
|
821
|
+
const remaining = expiresAt - Date.now();
|
|
822
|
+
if (remaining <= 0) return "expired";
|
|
823
|
+
if (remaining <= safetyMs) return "expiring_soon";
|
|
824
|
+
return "valid";
|
|
825
|
+
}
|
|
826
|
+
function generateSessionId() {
|
|
827
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
828
|
+
return crypto.randomUUID();
|
|
829
|
+
}
|
|
830
|
+
return `ps_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
831
|
+
}
|
|
832
|
+
function createVideoPlayer(options) {
|
|
833
|
+
assertNoSecretsInConfig(options);
|
|
834
|
+
let config = mergePlayerConfig({
|
|
835
|
+
player: options.player,
|
|
836
|
+
controls: options.controls,
|
|
837
|
+
theme: options.theme,
|
|
838
|
+
watermark: options.watermark,
|
|
839
|
+
streaming: options.streaming,
|
|
840
|
+
logging: options.logging,
|
|
841
|
+
debug: options.debug,
|
|
842
|
+
accessibility: options.accessibility
|
|
843
|
+
});
|
|
844
|
+
const events = new EventEmitter();
|
|
845
|
+
const logger = new Logger(config.logging);
|
|
846
|
+
const playbackSessionId = generateSessionId();
|
|
847
|
+
let state = "idle";
|
|
848
|
+
let destroyed = false;
|
|
849
|
+
let lastError = null;
|
|
850
|
+
let videoEl = options.mount ?? null;
|
|
851
|
+
let engine = null;
|
|
852
|
+
let refreshManager = null;
|
|
853
|
+
let fullscreenEl = null;
|
|
854
|
+
let isLiveStream = false;
|
|
855
|
+
const setState = (next) => {
|
|
856
|
+
if (destroyed && next !== "destroyed") return;
|
|
857
|
+
state = next;
|
|
858
|
+
events.emit("statechange", { state: next });
|
|
859
|
+
};
|
|
860
|
+
const authManager = new AuthManager(
|
|
861
|
+
options.auth,
|
|
862
|
+
options.auth?.refreshBeforeExpiryMs ?? 6e4,
|
|
863
|
+
logger,
|
|
864
|
+
(token) => events.emit("authrefresh", { expiresAt: token.expiresAt }),
|
|
865
|
+
(message, retryable) => events.emit("autherror", { message, retryable })
|
|
866
|
+
);
|
|
867
|
+
const sourceManager = new PlaybackSourceManager(options.playback, logger);
|
|
868
|
+
const ensureVideoElement = () => {
|
|
869
|
+
if (!videoEl) {
|
|
870
|
+
throw createPlayerError(
|
|
871
|
+
"INVALID_CONFIGURATION" /* INVALID_CONFIGURATION */,
|
|
872
|
+
"A video element must be provided via mount before starting playback",
|
|
873
|
+
{ category: "config", fatal: true, retryable: false }
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return videoEl;
|
|
877
|
+
};
|
|
878
|
+
const startPlayback = async () => {
|
|
879
|
+
if (destroyed) return;
|
|
880
|
+
setState("loading");
|
|
881
|
+
lastError = null;
|
|
882
|
+
const videoId = options.videoId ?? "unknown";
|
|
883
|
+
try {
|
|
884
|
+
await authManager.ensureToken({
|
|
885
|
+
reason: "initial",
|
|
886
|
+
videoId: options.videoId
|
|
887
|
+
});
|
|
888
|
+
let playbackUrl;
|
|
889
|
+
let expiresAt;
|
|
890
|
+
if (options.sourceUrl) {
|
|
891
|
+
const source = {
|
|
892
|
+
url: options.sourceUrl,
|
|
893
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1e3,
|
|
894
|
+
live: /\/hls-web-live\//i.test(options.sourceUrl)
|
|
895
|
+
};
|
|
896
|
+
sourceManager.applyDirectSource(source);
|
|
897
|
+
playbackUrl = source.url;
|
|
898
|
+
expiresAt = source.expiresAt;
|
|
899
|
+
isLiveStream = Boolean(source.live);
|
|
900
|
+
} else {
|
|
901
|
+
const source = await sourceManager.load({
|
|
902
|
+
videoId,
|
|
903
|
+
reason: "initial"
|
|
904
|
+
});
|
|
905
|
+
playbackUrl = source.url;
|
|
906
|
+
expiresAt = source.expiresAt;
|
|
907
|
+
isLiveStream = Boolean(source.live);
|
|
908
|
+
}
|
|
909
|
+
const el = ensureVideoElement();
|
|
910
|
+
engine?.destroy();
|
|
911
|
+
engine = new HlsEngine({
|
|
912
|
+
videoEl: el,
|
|
913
|
+
playbackUrl,
|
|
914
|
+
authParams: sourceManager.authParams,
|
|
915
|
+
autoplay: config.player.autoplay,
|
|
916
|
+
startMuted: config.player.startMuted,
|
|
917
|
+
startPosition: config.player.startPosition,
|
|
918
|
+
volume: config.player.volume,
|
|
919
|
+
hlsConfig: config.streaming.hls,
|
|
920
|
+
logger,
|
|
921
|
+
callbacks: {
|
|
922
|
+
onStateChange: (s) => {
|
|
923
|
+
setState(s);
|
|
924
|
+
if (s === "ready") events.emit("ready");
|
|
925
|
+
if (s === "playing") {
|
|
926
|
+
events.emit("play");
|
|
927
|
+
events.emit("playing");
|
|
928
|
+
}
|
|
929
|
+
if (s === "paused") events.emit("pause");
|
|
930
|
+
if (s === "buffering") events.emit("buffering");
|
|
931
|
+
if (s === "ended") events.emit("ended");
|
|
932
|
+
},
|
|
933
|
+
onFatalError: (message) => {
|
|
934
|
+
lastError = createPlayerError(
|
|
935
|
+
"PLAYBACK_ERROR" /* PLAYBACK_ERROR */,
|
|
936
|
+
message,
|
|
937
|
+
{ category: "playback", fatal: true, retryable: true }
|
|
938
|
+
);
|
|
939
|
+
setState("error");
|
|
940
|
+
events.emit("error", {
|
|
941
|
+
code: lastError.code,
|
|
942
|
+
message: lastError.message,
|
|
943
|
+
fatal: true
|
|
944
|
+
});
|
|
945
|
+
},
|
|
946
|
+
onAuthRecoveryNeeded: (reason) => {
|
|
947
|
+
refreshManager?.requestRenew(
|
|
948
|
+
reason
|
|
949
|
+
);
|
|
950
|
+
},
|
|
951
|
+
onQualitiesChange: () => {
|
|
952
|
+
},
|
|
953
|
+
onQualitySelectionChange: (selection) => {
|
|
954
|
+
const level = engine?.getQualities().find((q) => q.index === selection);
|
|
955
|
+
events.emit("qualitychange", {
|
|
956
|
+
selection,
|
|
957
|
+
label: typeof selection === "number" ? level?.label : "auto"
|
|
958
|
+
});
|
|
959
|
+
},
|
|
960
|
+
onEnded: () => events.emit("ended")
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
await engine.start();
|
|
964
|
+
if (!options.sourceUrl && options.playback) {
|
|
965
|
+
refreshManager?.stop();
|
|
966
|
+
refreshManager = new RefreshManager({
|
|
967
|
+
videoId,
|
|
968
|
+
authManager,
|
|
969
|
+
sourceManager,
|
|
970
|
+
refreshBeforeExpiryMs: options.playback.refreshBeforeExpiryMs ?? 3e4,
|
|
971
|
+
renewMaxRetries: options.playback.renewMaxRetries ?? 3,
|
|
972
|
+
renewBackoffMs: options.playback.renewBackoffMs ?? 2e3,
|
|
973
|
+
coalesceMs: options.playback.authRecoveryCoalesceMs ?? 1e3,
|
|
974
|
+
logger,
|
|
975
|
+
isDestroyed: () => destroyed,
|
|
976
|
+
onRenewing: () => setState("renewing"),
|
|
977
|
+
onRenewed: (nextExpiresAt) => {
|
|
978
|
+
engine?.resumeAfterAuthRenew();
|
|
979
|
+
events.emit("tokenrefresh", { expiresAt: nextExpiresAt });
|
|
980
|
+
events.emit("sourcechange", { expiresAt: nextExpiresAt });
|
|
981
|
+
},
|
|
982
|
+
onFatal: (message) => {
|
|
983
|
+
lastError = createPlayerError(
|
|
984
|
+
"TOKEN_REFRESH_FAILED" /* TOKEN_REFRESH_FAILED */,
|
|
985
|
+
message,
|
|
986
|
+
{ category: "auth", fatal: true, retryable: true }
|
|
987
|
+
);
|
|
988
|
+
setState("error");
|
|
989
|
+
events.emit("error", {
|
|
990
|
+
code: lastError.code,
|
|
991
|
+
message: lastError.message,
|
|
992
|
+
fatal: true
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
refreshManager.start(expiresAt);
|
|
997
|
+
}
|
|
998
|
+
options.analytics?.onEvent?.({
|
|
999
|
+
type: "PLAYER_INITIALIZED",
|
|
1000
|
+
payload: { videoId, playbackSessionId }
|
|
1001
|
+
});
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
const playerError = error && typeof error === "object" && "code" in error ? error : createPlayerError(
|
|
1004
|
+
"UNKNOWN" /* UNKNOWN */,
|
|
1005
|
+
error instanceof Error ? error.message : "Failed to start player",
|
|
1006
|
+
{ category: "unknown", fatal: true, retryable: true, cause: error }
|
|
1007
|
+
);
|
|
1008
|
+
lastError = playerError;
|
|
1009
|
+
setState("error");
|
|
1010
|
+
events.emit("error", {
|
|
1011
|
+
code: playerError.code,
|
|
1012
|
+
message: playerError.message,
|
|
1013
|
+
fatal: playerError.fatal
|
|
1014
|
+
});
|
|
1015
|
+
throw playerError;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
if (options.mount) {
|
|
1019
|
+
void startPlayback().catch(() => {
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
const api = {
|
|
1023
|
+
async play() {
|
|
1024
|
+
await ensureVideoElement().play();
|
|
1025
|
+
},
|
|
1026
|
+
pause() {
|
|
1027
|
+
ensureVideoElement().pause();
|
|
1028
|
+
},
|
|
1029
|
+
seek(time) {
|
|
1030
|
+
const el = ensureVideoElement();
|
|
1031
|
+
const duration = Number.isFinite(el.duration) ? el.duration : time;
|
|
1032
|
+
el.currentTime = Math.min(Math.max(0, time), duration);
|
|
1033
|
+
},
|
|
1034
|
+
goToLive() {
|
|
1035
|
+
const el = ensureVideoElement();
|
|
1036
|
+
return seekToLiveEdge(el);
|
|
1037
|
+
},
|
|
1038
|
+
isLive() {
|
|
1039
|
+
return isLiveStream;
|
|
1040
|
+
},
|
|
1041
|
+
isAtLiveEdge() {
|
|
1042
|
+
if (!isLiveStream || !videoEl) {
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
return isAtLiveEdge(videoEl);
|
|
1046
|
+
},
|
|
1047
|
+
getCurrentTime() {
|
|
1048
|
+
return videoEl?.currentTime ?? 0;
|
|
1049
|
+
},
|
|
1050
|
+
getDuration() {
|
|
1051
|
+
return videoEl?.duration ?? 0;
|
|
1052
|
+
},
|
|
1053
|
+
setVolume(volume) {
|
|
1054
|
+
const el = ensureVideoElement();
|
|
1055
|
+
el.volume = Math.min(1, Math.max(0, volume));
|
|
1056
|
+
events.emit("volumechange", { volume: el.volume, muted: el.muted });
|
|
1057
|
+
},
|
|
1058
|
+
getVolume() {
|
|
1059
|
+
return videoEl?.volume ?? 0;
|
|
1060
|
+
},
|
|
1061
|
+
mute() {
|
|
1062
|
+
const el = ensureVideoElement();
|
|
1063
|
+
el.muted = true;
|
|
1064
|
+
events.emit("volumechange", { volume: el.volume, muted: true });
|
|
1065
|
+
},
|
|
1066
|
+
unmute() {
|
|
1067
|
+
const el = ensureVideoElement();
|
|
1068
|
+
el.muted = false;
|
|
1069
|
+
events.emit("volumechange", { volume: el.volume, muted: false });
|
|
1070
|
+
},
|
|
1071
|
+
isMuted() {
|
|
1072
|
+
return videoEl?.muted ?? false;
|
|
1073
|
+
},
|
|
1074
|
+
setPlaybackRate(rate) {
|
|
1075
|
+
ensureVideoElement().playbackRate = rate;
|
|
1076
|
+
},
|
|
1077
|
+
getPlaybackRate() {
|
|
1078
|
+
return videoEl?.playbackRate ?? 1;
|
|
1079
|
+
},
|
|
1080
|
+
getState() {
|
|
1081
|
+
return state;
|
|
1082
|
+
},
|
|
1083
|
+
getBufferedRanges() {
|
|
1084
|
+
const el = videoEl;
|
|
1085
|
+
if (!el) return [];
|
|
1086
|
+
const ranges = [];
|
|
1087
|
+
for (let i = 0; i < el.buffered.length; i += 1) {
|
|
1088
|
+
ranges.push({ start: el.buffered.start(i), end: el.buffered.end(i) });
|
|
1089
|
+
}
|
|
1090
|
+
return ranges;
|
|
1091
|
+
},
|
|
1092
|
+
getQuality() {
|
|
1093
|
+
return engine?.getQuality() ?? "auto";
|
|
1094
|
+
},
|
|
1095
|
+
setQuality(selection) {
|
|
1096
|
+
if (selection === "auto") {
|
|
1097
|
+
engine?.setQuality("auto");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (typeof selection === "number") {
|
|
1101
|
+
engine?.setQuality(selection);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const match = engine?.getQualities().find((q) => q.label === selection || `${q.height}p` === selection);
|
|
1105
|
+
if (match) engine?.setQuality(match.index);
|
|
1106
|
+
},
|
|
1107
|
+
getAvailableQualities() {
|
|
1108
|
+
return engine?.getQualities() ?? [];
|
|
1109
|
+
},
|
|
1110
|
+
async enterFullscreen() {
|
|
1111
|
+
const target = fullscreenEl ?? videoEl?.parentElement ?? videoEl;
|
|
1112
|
+
if (target && "requestFullscreen" in target) {
|
|
1113
|
+
await target.requestFullscreen();
|
|
1114
|
+
events.emit("fullscreenchange", { isFullscreen: true });
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
async exitFullscreen() {
|
|
1118
|
+
if (document.fullscreenElement) {
|
|
1119
|
+
await document.exitFullscreen();
|
|
1120
|
+
events.emit("fullscreenchange", { isFullscreen: false });
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
isFullscreen() {
|
|
1124
|
+
return Boolean(document.fullscreenElement);
|
|
1125
|
+
},
|
|
1126
|
+
async reload() {
|
|
1127
|
+
refreshManager?.stop();
|
|
1128
|
+
engine?.destroy();
|
|
1129
|
+
await startPlayback();
|
|
1130
|
+
},
|
|
1131
|
+
destroy() {
|
|
1132
|
+
if (destroyed) return;
|
|
1133
|
+
destroyed = true;
|
|
1134
|
+
refreshManager?.stop();
|
|
1135
|
+
refreshManager = null;
|
|
1136
|
+
engine?.destroy();
|
|
1137
|
+
engine = null;
|
|
1138
|
+
authManager.destroy();
|
|
1139
|
+
sourceManager.destroy();
|
|
1140
|
+
setState("destroyed");
|
|
1141
|
+
events.emit("destroy");
|
|
1142
|
+
events.removeAll();
|
|
1143
|
+
options.analytics?.onEvent?.({ type: "PLAYER_DESTROYED" });
|
|
1144
|
+
},
|
|
1145
|
+
getDiagnostics() {
|
|
1146
|
+
const buffered = api.getBufferedRanges();
|
|
1147
|
+
return {
|
|
1148
|
+
playerVersion: PACKAGE_VERSION,
|
|
1149
|
+
currentState: state,
|
|
1150
|
+
hlsSupport: engine?.getSupportMode() ?? "unsupported",
|
|
1151
|
+
videoId: options.videoId ?? null,
|
|
1152
|
+
currentTime: api.getCurrentTime(),
|
|
1153
|
+
duration: api.getDuration(),
|
|
1154
|
+
bufferedEnd: buffered.length ? buffered[buffered.length - 1].end : 0,
|
|
1155
|
+
currentQuality: api.getQuality() === "auto" ? "auto" : engine?.getQualities().find((q) => q.index === api.getQuality())?.label ?? null,
|
|
1156
|
+
playbackSessionId,
|
|
1157
|
+
authExpiryStatus: expiryStatus(
|
|
1158
|
+
authManager.getExpiresAt(),
|
|
1159
|
+
options.auth?.refreshBeforeExpiryMs ?? 6e4
|
|
1160
|
+
),
|
|
1161
|
+
sourceExpiryStatus: expiryStatus(
|
|
1162
|
+
sourceManager.getExpiresAt(),
|
|
1163
|
+
options.playback?.refreshBeforeExpiryMs ?? 3e4
|
|
1164
|
+
),
|
|
1165
|
+
lastError: lastError?.message ?? null
|
|
1166
|
+
};
|
|
1167
|
+
},
|
|
1168
|
+
getConfig() {
|
|
1169
|
+
return config;
|
|
1170
|
+
},
|
|
1171
|
+
updateConfig(partial) {
|
|
1172
|
+
config = mergePlayerConfig({
|
|
1173
|
+
player: { ...config.player, ...partial?.player },
|
|
1174
|
+
controls: { ...config.controls, ...partial?.controls },
|
|
1175
|
+
theme: { ...config.theme, ...partial?.theme },
|
|
1176
|
+
watermark: { ...config.watermark, ...partial?.watermark },
|
|
1177
|
+
streaming: {
|
|
1178
|
+
engine: "hls",
|
|
1179
|
+
hls: { ...config.streaming.hls, ...partial?.streaming?.hls }
|
|
1180
|
+
},
|
|
1181
|
+
logging: { ...config.logging, ...partial?.logging },
|
|
1182
|
+
debug: { ...config.debug, ...partial?.debug },
|
|
1183
|
+
accessibility: {
|
|
1184
|
+
...config.accessibility,
|
|
1185
|
+
...partial?.accessibility
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
logger.update(config.logging);
|
|
1189
|
+
},
|
|
1190
|
+
on: events.on.bind(events),
|
|
1191
|
+
off: events.off.bind(events)
|
|
1192
|
+
};
|
|
1193
|
+
api.attach = async (el, container) => {
|
|
1194
|
+
videoEl = el;
|
|
1195
|
+
fullscreenEl = container ?? null;
|
|
1196
|
+
await startPlayback();
|
|
1197
|
+
};
|
|
1198
|
+
return api;
|
|
1199
|
+
}
|
|
1200
|
+
export {
|
|
1201
|
+
AuthManager,
|
|
1202
|
+
AuthParamsStore,
|
|
1203
|
+
DEFAULT_CONTROLS,
|
|
1204
|
+
DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS,
|
|
1205
|
+
DEFAULT_PLAYER_BEHAVIOR,
|
|
1206
|
+
DEFAULT_PLAYER_CONFIG,
|
|
1207
|
+
DEFAULT_THEME,
|
|
1208
|
+
DEFAULT_WATERMARK,
|
|
1209
|
+
EventEmitter,
|
|
1210
|
+
PACKAGE_VERSION,
|
|
1211
|
+
PLAYBACK_RATES,
|
|
1212
|
+
PlaybackSourceManager,
|
|
1213
|
+
PlayerErrorCode,
|
|
1214
|
+
RefreshManager,
|
|
1215
|
+
assertNoSecretsInConfig,
|
|
1216
|
+
createPlayerError,
|
|
1217
|
+
createVideoPlayer,
|
|
1218
|
+
getLiveEdgeTime,
|
|
1219
|
+
isAtLiveEdge,
|
|
1220
|
+
mergePlayerConfig,
|
|
1221
|
+
redactObject,
|
|
1222
|
+
redactSensitive,
|
|
1223
|
+
redactValue,
|
|
1224
|
+
seekToLiveEdge,
|
|
1225
|
+
themeToCssVars
|
|
1226
|
+
};
|
|
1227
|
+
//# sourceMappingURL=index.js.map
|