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