@scarlett-player/embed 1.1.1 → 1.4.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.
@@ -1,516 +1,231 @@
1
- function formatLevel(level) {
2
- if (level.name) {
3
- return level.name;
1
+ class Signal {
2
+ constructor(initialValue) {
3
+ this.subscribers = /* @__PURE__ */ new Set();
4
+ this.value = initialValue;
4
5
  }
5
- if (level.height) {
6
- const standardLabels = {
7
- 2160: "4K",
8
- 1440: "1440p",
9
- 1080: "1080p",
10
- 720: "720p",
11
- 480: "480p",
12
- 360: "360p",
13
- 240: "240p",
14
- 144: "144p"
15
- };
16
- const closest = Object.keys(standardLabels).map(Number).sort((a, b) => Math.abs(a - level.height) - Math.abs(b - level.height))[0];
17
- if (Math.abs(closest - level.height) <= 20) {
18
- return standardLabels[closest];
6
+ /**
7
+ * Get the current value and track dependency if called within an effect.
8
+ *
9
+ * @returns Current value
10
+ */
11
+ get() {
12
+ return this.value;
13
+ }
14
+ /**
15
+ * Set a new value and notify subscribers if changed.
16
+ *
17
+ * @param newValue - New value to set
18
+ */
19
+ set(newValue) {
20
+ if (Object.is(this.value, newValue)) {
21
+ return;
19
22
  }
20
- return `${level.height}p`;
23
+ this.value = newValue;
24
+ this.notify();
21
25
  }
22
- if (level.bitrate) {
23
- return formatBitrate(level.bitrate);
26
+ /**
27
+ * Update the value using a function.
28
+ *
29
+ * @param updater - Function that receives current value and returns new value
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const count = new Signal(0);
34
+ * count.update(n => n + 1); // Increments by 1
35
+ * ```
36
+ */
37
+ update(updater) {
38
+ this.set(updater(this.value));
24
39
  }
25
- return "Unknown";
26
- }
27
- function formatBitrate(bitrate) {
28
- if (bitrate >= 1e6) {
29
- return `${(bitrate / 1e6).toFixed(1)} Mbps`;
40
+ /**
41
+ * Subscribe to changes without automatic dependency tracking.
42
+ *
43
+ * @param callback - Function to call when value changes
44
+ * @returns Unsubscribe function
45
+ */
46
+ subscribe(callback) {
47
+ this.subscribers.add(callback);
48
+ return () => this.subscribers.delete(callback);
30
49
  }
31
- if (bitrate >= 1e3) {
32
- return `${Math.round(bitrate / 1e3)} Kbps`;
50
+ /**
51
+ * Notify all subscribers of a change.
52
+ * @internal
53
+ */
54
+ notify() {
55
+ this.subscribers.forEach((subscriber) => {
56
+ try {
57
+ subscriber();
58
+ } catch (error) {
59
+ console.error("[Scarlett Player] Error in signal subscriber:", error);
60
+ }
61
+ });
33
62
  }
34
- return `${bitrate} bps`;
35
- }
36
- function mapLevels(levels, _currentLevel) {
37
- return levels.map((level, index) => ({
38
- index,
39
- width: level.width || 0,
40
- height: level.height || 0,
41
- bitrate: level.bitrate || 0,
42
- label: formatLevel(level),
43
- codec: level.codecSet
44
- }));
45
- }
46
- function getInitialBandwidthEstimate(overrideBps) {
47
- const HLS_DEFAULT_ESTIMATE = 5e5;
48
- if (overrideBps !== void 0 && overrideBps > 0) {
49
- return overrideBps;
63
+ /**
64
+ * Clean up all subscriptions.
65
+ * Call this when destroying the signal.
66
+ */
67
+ destroy() {
68
+ this.subscribers.clear();
50
69
  }
51
- const connection = navigator.connection;
52
- if (connection?.downlink && connection.downlink > 0) {
53
- const bps = connection.downlink * 1e6;
54
- return Math.round(bps * 0.85);
70
+ /**
71
+ * Get the current number of subscribers (for debugging).
72
+ * @internal
73
+ */
74
+ getSubscriberCount() {
75
+ return this.subscribers.size;
55
76
  }
56
- return HLS_DEFAULT_ESTIMATE;
57
77
  }
58
- var HLS_ERROR_TYPES = {
59
- NETWORK_ERROR: "networkError",
60
- MEDIA_ERROR: "mediaError",
61
- MUX_ERROR: "muxError"
78
+ function signal(initialValue) {
79
+ return new Signal(initialValue);
80
+ }
81
+ const DEFAULT_STATE = {
82
+ // Core Playback State
83
+ playbackState: "idle",
84
+ playing: false,
85
+ paused: true,
86
+ ended: false,
87
+ buffering: false,
88
+ waiting: false,
89
+ seeking: false,
90
+ // Time & Duration
91
+ currentTime: 0,
92
+ duration: NaN,
93
+ buffered: null,
94
+ bufferedAmount: 0,
95
+ // Media Info
96
+ mediaType: "unknown",
97
+ source: null,
98
+ title: "",
99
+ poster: "",
100
+ // Volume & Audio
101
+ volume: 1,
102
+ muted: false,
103
+ // Playback Controls
104
+ playbackRate: 1,
105
+ fullscreen: false,
106
+ pip: false,
107
+ controlsVisible: true,
108
+ // Quality & Tracks
109
+ qualities: [],
110
+ currentQuality: null,
111
+ audioTracks: [],
112
+ currentAudioTrack: null,
113
+ textTracks: [],
114
+ currentTextTrack: null,
115
+ // Live/DVR State (TSP features)
116
+ live: false,
117
+ liveEdge: true,
118
+ seekableRange: null,
119
+ liveLatency: 0,
120
+ lowLatencyMode: false,
121
+ // Chapters (TSP features)
122
+ chapters: [],
123
+ currentChapter: null,
124
+ // Error State
125
+ error: null,
126
+ // Network & Performance
127
+ bandwidth: 0,
128
+ autoplay: false,
129
+ loop: false,
130
+ // Casting State
131
+ airplayAvailable: false,
132
+ airplayActive: false,
133
+ chromecastAvailable: false,
134
+ chromecastActive: false,
135
+ // Thumbnail Preview
136
+ thumbnails: null,
137
+ // UI State
138
+ interacting: false,
139
+ hovering: false,
140
+ focused: false
62
141
  };
63
- function mapErrorType(hlsType) {
64
- switch (hlsType) {
65
- case HLS_ERROR_TYPES.NETWORK_ERROR:
66
- return "network";
67
- case HLS_ERROR_TYPES.MEDIA_ERROR:
68
- return "media";
69
- case HLS_ERROR_TYPES.MUX_ERROR:
70
- return "mux";
71
- default:
72
- return "other";
142
+ class StateManager {
143
+ /**
144
+ * Create a new StateManager with default initial state.
145
+ *
146
+ * @param initialState - Optional partial initial state (merged with defaults)
147
+ */
148
+ constructor(initialState) {
149
+ this.signals = /* @__PURE__ */ new Map();
150
+ this.changeSubscribers = /* @__PURE__ */ new Set();
151
+ this.definedDefaults = /* @__PURE__ */ new Map();
152
+ this.initializeSignals(initialState);
73
153
  }
74
- }
75
- function parseHlsError(data) {
76
- return {
77
- type: mapErrorType(data.type),
78
- details: data.details || "Unknown error",
79
- fatal: data.fatal || false,
80
- url: data.url,
81
- reason: data.reason,
82
- response: data.response
83
- };
84
- }
85
- function setupHlsEventHandlers(hls, api, callbacks) {
86
- const handlers = [];
87
- const addHandler = (event, handler) => {
88
- hls.on(event, handler);
89
- handlers.push({ event, handler });
90
- };
91
- addHandler("hlsManifestParsed", (_event, data) => {
92
- api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
93
- const levels = data.levels.map((level, index) => ({
94
- id: `level-${index}`,
95
- label: formatLevel(level),
96
- width: level.width,
97
- height: level.height,
98
- bitrate: level.bitrate,
99
- active: index === hls.currentLevel
100
- }));
101
- api.setState("qualities", levels);
102
- api.emit("quality:levels", {
103
- levels: levels.map((l) => ({ id: l.id, label: l.label }))
154
+ /**
155
+ * Initialize all state signals with default or provided values.
156
+ * @private
157
+ */
158
+ initializeSignals(overrides) {
159
+ const initialState = { ...DEFAULT_STATE, ...overrides };
160
+ for (const [key, value] of Object.entries(initialState)) {
161
+ this.createSignal(key, value);
162
+ }
163
+ }
164
+ /**
165
+ * Create and register a signal, wired to the global change subscribers.
166
+ *
167
+ * Shared by initializeSignals() and define() so a plugin-defined key behaves
168
+ * exactly like a built-in one and the two paths cannot drift apart.
169
+ *
170
+ * @private
171
+ */
172
+ createSignal(key, value) {
173
+ const stateSignal = signal(value);
174
+ stateSignal.subscribe(() => {
175
+ this.notifyChangeSubscribers(key);
104
176
  });
105
- callbacks.onManifestParsed?.(data.levels);
106
- });
107
- addHandler("hlsLevelSwitched", (_event, data) => {
108
- const level = hls.levels[data.level];
109
- const isAuto = callbacks.getIsAutoQuality?.() ?? hls.autoLevelEnabled;
110
- api.logger.debug("HLS level switched", { level: data.level, height: level?.height, auto: isAuto });
111
- if (level) {
112
- const label = isAuto ? `Auto (${formatLevel(level)})` : formatLevel(level);
113
- api.setState("currentQuality", {
114
- id: isAuto ? "auto" : `level-${data.level}`,
115
- label,
116
- width: level.width,
117
- height: level.height,
118
- bitrate: level.bitrate,
119
- active: true
120
- });
121
- }
122
- api.emit("quality:change", {
123
- quality: level ? formatLevel(level) : "auto",
124
- auto: isAuto
125
- });
126
- callbacks.onLevelSwitched?.(data.level);
127
- });
128
- let lastBandwidthUpdate = 0;
129
- addHandler("hlsFragLoaded", () => {
130
- const now = Date.now();
131
- if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
132
- lastBandwidthUpdate = now;
133
- api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
134
- }
135
- callbacks.onFragLoaded?.();
136
- });
137
- addHandler("hlsFragBuffered", () => {
138
- api.setState("buffering", false);
139
- callbacks.onBufferUpdate?.();
140
- });
141
- addHandler("hlsFragLoading", () => {
142
- api.setState("buffering", true);
143
- });
144
- addHandler("hlsLevelLoaded", (_event, data) => {
145
- if (data.details?.live !== void 0) {
146
- api.setState("live", data.details.live);
147
- if (data.details.live) {
148
- const video = hls.media;
149
- if (video && video.seekable && video.seekable.length > 0) {
150
- const start = video.seekable.start(0);
151
- const end = video.seekable.end(video.seekable.length - 1);
152
- api.setState("seekableRange", { start, end });
153
- const threshold = (data.details.targetduration ?? 3) * 3;
154
- const isAtLiveEdge = end - video.currentTime < threshold;
155
- api.setState("liveEdge", isAtLiveEdge);
156
- const latency = end - video.currentTime;
157
- api.setState("liveLatency", Math.max(0, latency));
158
- }
159
- }
160
- callbacks.onLiveUpdate?.();
161
- }
162
- });
163
- addHandler("hlsError", (_event, data) => {
164
- const error = parseHlsError(data);
165
- const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
166
- if (isBufferHoleSeek) {
167
- api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
168
- details: error.details,
169
- reason: error.reason
170
- });
171
- } else if (error.fatal) {
172
- api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
173
- type: error.type,
174
- details: error.details,
175
- url: error.url
176
- });
177
- } else {
178
- api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
179
- type: error.type,
180
- details: error.details,
181
- fatal: error.fatal,
182
- url: error.url
183
- });
184
- }
185
- callbacks.onError?.(error);
186
- });
187
- return () => {
188
- for (const { event, handler } of handlers) {
189
- hls.off(event, handler);
190
- }
191
- handlers.length = 0;
192
- };
193
- }
194
- function setupVideoEventHandlers(video, api) {
195
- const handlers = [];
196
- const addHandler = (event, handler) => {
197
- video.addEventListener(event, handler);
198
- handlers.push({ event, handler });
199
- };
200
- addHandler("play", () => {
201
- api.setState("paused", false);
202
- });
203
- addHandler("playing", () => {
204
- api.setState("playing", true);
205
- api.setState("paused", false);
206
- api.setState("waiting", false);
207
- api.setState("buffering", false);
208
- api.setState("playbackState", "playing");
209
- });
210
- addHandler("pause", () => {
211
- api.setState("playing", false);
212
- api.setState("paused", true);
213
- api.setState("playbackState", "paused");
214
- });
215
- addHandler("ended", () => {
216
- api.setState("playing", false);
217
- api.setState("ended", true);
218
- api.setState("playbackState", "ended");
219
- api.emit("playback:ended", void 0);
220
- });
221
- addHandler("timeupdate", () => {
222
- api.setState("currentTime", video.currentTime);
223
- api.emit("playback:timeupdate", { currentTime: video.currentTime });
224
- const isLive = api.getState("live");
225
- if (isLive && video.seekable && video.seekable.length > 0) {
226
- const start = video.seekable.start(0);
227
- const end = video.seekable.end(video.seekable.length - 1);
228
- api.setState("seekableRange", { start, end });
229
- const isAtLiveEdge = end - video.currentTime < 10;
230
- api.setState("liveEdge", isAtLiveEdge);
231
- api.setState("liveLatency", Math.max(0, end - video.currentTime));
232
- }
233
- });
234
- addHandler("durationchange", () => {
235
- api.setState("duration", video.duration || 0);
236
- api.emit("media:loadedmetadata", { duration: video.duration || 0 });
237
- });
238
- addHandler("waiting", () => {
239
- api.setState("waiting", true);
240
- api.setState("buffering", true);
241
- api.emit("media:waiting", void 0);
242
- });
243
- addHandler("canplay", () => {
244
- api.setState("waiting", false);
245
- api.setState("playbackState", "ready");
246
- api.emit("media:canplay", void 0);
247
- });
248
- addHandler("canplaythrough", () => {
249
- api.setState("buffering", false);
250
- api.emit("media:canplaythrough", void 0);
251
- });
252
- addHandler("progress", () => {
253
- if (video.buffered.length > 0) {
254
- const bufferedEnd = video.buffered.end(video.buffered.length - 1);
255
- const bufferedAmount = video.duration > 0 ? bufferedEnd / video.duration : 0;
256
- api.setState("bufferedAmount", bufferedAmount);
257
- api.setState("buffered", video.buffered);
258
- api.emit("media:progress", { buffered: bufferedAmount });
259
- }
260
- });
261
- addHandler("seeking", () => {
262
- api.setState("seeking", true);
263
- });
264
- addHandler("seeked", () => {
265
- api.setState("seeking", false);
266
- api.emit("playback:seeked", { time: video.currentTime });
267
- });
268
- addHandler("volumechange", () => {
269
- api.setState("volume", video.volume);
270
- api.setState("muted", video.muted);
271
- api.emit("volume:change", { volume: video.volume, muted: video.muted });
272
- });
273
- addHandler("ratechange", () => {
274
- api.setState("playbackRate", video.playbackRate);
275
- api.emit("playback:ratechange", { rate: video.playbackRate });
276
- });
277
- addHandler("loadedmetadata", () => {
278
- api.setState("duration", video.duration);
279
- api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
280
- });
281
- addHandler("loadeddata", () => {
282
- if (video.videoWidth > 0) {
283
- api.setState("mediaType", "video");
284
- }
285
- });
286
- addHandler("error", () => {
287
- const error = video.error;
288
- if (error) {
289
- api.logger.error("Video element error", { code: error.code, message: error.message });
290
- api.emit("media:error", { error: new Error(error.message || "Video playback error") });
291
- }
292
- });
293
- addHandler("enterpictureinpicture", () => {
294
- api.setState("pip", true);
295
- api.logger.debug("PiP: entered (standard)");
296
- });
297
- addHandler("leavepictureinpicture", () => {
298
- api.setState("pip", false);
299
- api.logger.debug("PiP: exited (standard)");
300
- if (!video.paused || api.getState("playing")) {
301
- video.play().catch(() => {
302
- });
303
- }
304
- });
305
- const webkitVideo = video;
306
- if ("webkitPresentationMode" in video) {
307
- addHandler("webkitpresentationmodechanged", () => {
308
- const mode = webkitVideo.webkitPresentationMode;
309
- const isInPip = mode === "picture-in-picture";
310
- api.setState("pip", isInPip);
311
- api.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
312
- if (mode === "inline" && video.paused) {
313
- video.play().catch(() => {
314
- });
315
- }
316
- });
317
- }
318
- return () => {
319
- for (const { event, handler } of handlers) {
320
- video.removeEventListener(event, handler);
321
- }
322
- handlers.length = 0;
323
- };
324
- }
325
- class Signal {
326
- constructor(initialValue) {
327
- this.subscribers = /* @__PURE__ */ new Set();
328
- this.value = initialValue;
177
+ this.signals.set(key, stateSignal);
329
178
  }
330
179
  /**
331
- * Get the current value and track dependency if called within an effect.
180
+ * Register a state key at runtime, for state a plugin owns.
332
181
  *
333
- * @returns Current value
334
- */
335
- get() {
336
- return this.value;
337
- }
338
- /**
339
- * Set a new value and notify subscribers if changed.
182
+ * Core cannot know every plugin's keys, and {@link get} deliberately throws
183
+ * for unregistered ones — that throw is a useful typo-catcher and is worth
184
+ * keeping — so a plugin declares its keys before first use.
340
185
  *
341
- * @param newValue - New value to set
186
+ * Idempotent by design: re-defining an existing key leaves the current value
187
+ * untouched. Plugins commonly re-run setup after a source change, and that
188
+ * must not reset state that is already live.
189
+ *
190
+ * Namespace plugin keys with the plugin's own name to avoid collisions.
191
+ *
192
+ * @param key - State property key
193
+ * @param initialValue - Value used only when the key is new
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * state.define('highlightSelection', null);
198
+ * ```
342
199
  */
343
- set(newValue) {
344
- if (Object.is(this.value, newValue)) {
200
+ define(key, initialValue) {
201
+ if (this.signals.has(key)) {
345
202
  return;
346
203
  }
347
- this.value = newValue;
348
- this.notify();
204
+ this.definedDefaults.set(key, initialValue);
205
+ this.createSignal(key, initialValue);
349
206
  }
350
207
  /**
351
- * Update the value using a function.
208
+ * Get the signal for a state property.
352
209
  *
353
- * @param updater - Function that receives current value and returns new value
210
+ * @param key - State property key
211
+ * @returns Signal for the property
354
212
  *
355
213
  * @example
356
214
  * ```ts
357
- * const count = new Signal(0);
358
- * count.update(n => n + 1); // Increments by 1
215
+ * const playingSignal = state.get('playing');
216
+ * playingSignal.get(); // false
217
+ * playingSignal.set(true);
359
218
  * ```
360
219
  */
361
- update(updater) {
362
- this.set(updater(this.value));
220
+ get(key) {
221
+ const stateSignal = this.signals.get(key);
222
+ if (!stateSignal) {
223
+ throw new Error(`[StateManager] Unknown state key: ${key}`);
224
+ }
225
+ return stateSignal;
363
226
  }
364
227
  /**
365
- * Subscribe to changes without automatic dependency tracking.
366
- *
367
- * @param callback - Function to call when value changes
368
- * @returns Unsubscribe function
369
- */
370
- subscribe(callback) {
371
- this.subscribers.add(callback);
372
- return () => this.subscribers.delete(callback);
373
- }
374
- /**
375
- * Notify all subscribers of a change.
376
- * @internal
377
- */
378
- notify() {
379
- this.subscribers.forEach((subscriber) => {
380
- try {
381
- subscriber();
382
- } catch (error) {
383
- console.error("[Scarlett Player] Error in signal subscriber:", error);
384
- }
385
- });
386
- }
387
- /**
388
- * Clean up all subscriptions.
389
- * Call this when destroying the signal.
390
- */
391
- destroy() {
392
- this.subscribers.clear();
393
- }
394
- /**
395
- * Get the current number of subscribers (for debugging).
396
- * @internal
397
- */
398
- getSubscriberCount() {
399
- return this.subscribers.size;
400
- }
401
- }
402
- function signal(initialValue) {
403
- return new Signal(initialValue);
404
- }
405
- const DEFAULT_STATE = {
406
- // Core Playback State
407
- playbackState: "idle",
408
- playing: false,
409
- paused: true,
410
- ended: false,
411
- buffering: false,
412
- waiting: false,
413
- seeking: false,
414
- // Time & Duration
415
- currentTime: 0,
416
- duration: NaN,
417
- buffered: null,
418
- bufferedAmount: 0,
419
- // Media Info
420
- mediaType: "unknown",
421
- source: null,
422
- title: "",
423
- poster: "",
424
- // Volume & Audio
425
- volume: 1,
426
- muted: false,
427
- // Playback Controls
428
- playbackRate: 1,
429
- fullscreen: false,
430
- pip: false,
431
- controlsVisible: true,
432
- // Quality & Tracks
433
- qualities: [],
434
- currentQuality: null,
435
- audioTracks: [],
436
- currentAudioTrack: null,
437
- textTracks: [],
438
- currentTextTrack: null,
439
- // Live/DVR State (TSP features)
440
- live: false,
441
- liveEdge: true,
442
- seekableRange: null,
443
- liveLatency: 0,
444
- lowLatencyMode: false,
445
- // Chapters (TSP features)
446
- chapters: [],
447
- currentChapter: null,
448
- // Error State
449
- error: null,
450
- // Network & Performance
451
- bandwidth: 0,
452
- autoplay: false,
453
- loop: false,
454
- // Casting State
455
- airplayAvailable: false,
456
- airplayActive: false,
457
- chromecastAvailable: false,
458
- chromecastActive: false,
459
- // Thumbnail Preview
460
- thumbnails: null,
461
- // UI State
462
- interacting: false,
463
- hovering: false,
464
- focused: false
465
- };
466
- class StateManager {
467
- /**
468
- * Create a new StateManager with default initial state.
469
- *
470
- * @param initialState - Optional partial initial state (merged with defaults)
471
- */
472
- constructor(initialState) {
473
- this.signals = /* @__PURE__ */ new Map();
474
- this.changeSubscribers = /* @__PURE__ */ new Set();
475
- this.initializeSignals(initialState);
476
- }
477
- /**
478
- * Initialize all state signals with default or provided values.
479
- * @private
480
- */
481
- initializeSignals(overrides) {
482
- const initialState = { ...DEFAULT_STATE, ...overrides };
483
- for (const [key, value] of Object.entries(initialState)) {
484
- const stateKey = key;
485
- const stateSignal = signal(value);
486
- stateSignal.subscribe(() => {
487
- this.notifyChangeSubscribers(stateKey);
488
- });
489
- this.signals.set(stateKey, stateSignal);
490
- }
491
- }
492
- /**
493
- * Get the signal for a state property.
494
- *
495
- * @param key - State property key
496
- * @returns Signal for the property
497
- *
498
- * @example
499
- * ```ts
500
- * const playingSignal = state.get('playing');
501
- * playingSignal.get(); // false
502
- * playingSignal.set(true);
503
- * ```
504
- */
505
- get(key) {
506
- const stateSignal = this.signals.get(key);
507
- if (!stateSignal) {
508
- throw new Error(`[StateManager] Unknown state key: ${key}`);
509
- }
510
- return stateSignal;
511
- }
512
- /**
513
- * Get the current value of a state property (convenience method).
228
+ * Get the current value of a state property (convenience method).
514
229
  *
515
230
  * @param key - State property key
516
231
  * @returns Current value
@@ -631,7 +346,10 @@ class StateManager {
631
346
  * ```
632
347
  */
633
348
  reset() {
634
- this.update(DEFAULT_STATE);
349
+ this.update({
350
+ ...DEFAULT_STATE,
351
+ ...Object.fromEntries(this.definedDefaults)
352
+ });
635
353
  }
636
354
  /**
637
355
  * Reset a specific state property to its default value.
@@ -644,7 +362,7 @@ class StateManager {
644
362
  * ```
645
363
  */
646
364
  resetKey(key) {
647
- const defaultValue = DEFAULT_STATE[key];
365
+ const defaultValue = key in DEFAULT_STATE ? DEFAULT_STATE[key] : this.definedDefaults.get(key);
648
366
  this.set(key, defaultValue);
649
367
  }
650
368
  /**
@@ -1248,6 +966,9 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
1248
966
  ErrorCode2["PLAYBACK_FAILED"] = "PLAYBACK_FAILED";
1249
967
  ErrorCode2["MEDIA_DECODE_ERROR"] = "MEDIA_DECODE_ERROR";
1250
968
  ErrorCode2["MEDIA_NETWORK_ERROR"] = "MEDIA_NETWORK_ERROR";
969
+ ErrorCode2["MEDIA_APPEND_ERROR"] = "MEDIA_APPEND_ERROR";
970
+ ErrorCode2["MEDIA_BUFFER_FULL"] = "MEDIA_BUFFER_FULL";
971
+ ErrorCode2["PLAYLIST_INVALID"] = "PLAYLIST_INVALID";
1251
972
  ErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
1252
973
  return ErrorCode2;
1253
974
  })(ErrorCode || {});
@@ -1290,6 +1011,23 @@ class ErrorHandler {
1290
1011
  this.eventBus.emit("error", playerError);
1291
1012
  return playerError;
1292
1013
  }
1014
+ /**
1015
+ * Record an error into history and logs WITHOUT emitting an `error` event.
1016
+ *
1017
+ * Used for advisory channels (e.g. media element errors that a provider's
1018
+ * recovery path is already handling) that should be visible in
1019
+ * getHistory() for diagnostics but must not flip the player's error state.
1020
+ *
1021
+ * @param error - Error to record (native or PlayerError)
1022
+ * @param context - Optional context (what was happening)
1023
+ * @returns Normalized PlayerError
1024
+ */
1025
+ record(error, context) {
1026
+ const playerError = this.normalizeError(error, context);
1027
+ this.addToHistory(playerError);
1028
+ this.logError(playerError);
1029
+ return playerError;
1030
+ }
1293
1031
  /**
1294
1032
  * Create and handle an error from code.
1295
1033
  *
@@ -1400,6 +1138,12 @@ class ErrorHandler {
1400
1138
  */
1401
1139
  getErrorCode(error) {
1402
1140
  const message = error.message.toLowerCase();
1141
+ if (message.includes("quota")) {
1142
+ return "MEDIA_BUFFER_FULL";
1143
+ }
1144
+ if (message.includes("append") || message.includes("sourcebuffer") || message.includes("arraybuffer")) {
1145
+ return "MEDIA_APPEND_ERROR";
1146
+ }
1403
1147
  if (message.includes("network")) {
1404
1148
  return "MEDIA_NETWORK_ERROR";
1405
1149
  }
@@ -1512,6 +1256,18 @@ class PluginAPI {
1512
1256
  setState(key, value) {
1513
1257
  this.stateManager.set(key, value);
1514
1258
  }
1259
+ /**
1260
+ * Register a state key this plugin owns, before first use.
1261
+ *
1262
+ * Idempotent — re-defining an existing key keeps its current value.
1263
+ * See {@link IPluginAPI.defineState}.
1264
+ *
1265
+ * @param key - State property key
1266
+ * @param initialValue - Value used only when the key is new
1267
+ */
1268
+ defineState(key, initialValue) {
1269
+ this.stateManager.define(key, initialValue);
1270
+ }
1515
1271
  /**
1516
1272
  * Subscribe to an event.
1517
1273
  *
@@ -1845,6 +1601,9 @@ class ScarlettPlayer {
1845
1601
  this.eventBus.on("media:loaded", () => {
1846
1602
  this.stateManager.set("error", null);
1847
1603
  });
1604
+ this.eventBus.on("media:error", ({ error }) => {
1605
+ this.errorHandler.record(error, { channel: "media:error" });
1606
+ });
1848
1607
  if (options.plugins) {
1849
1608
  for (const plugin of options.plugins) {
1850
1609
  this.pluginManager.register(plugin);
@@ -2473,105 +2232,426 @@ class ScarlettPlayer {
2473
2232
  if (this.destroyed) {
2474
2233
  throw new Error("Cannot call methods on destroyed player");
2475
2234
  }
2476
- }
2477
- /**
2478
- * Detect MIME type from source URL.
2479
- * @private
2480
- */
2481
- detectMimeType(source) {
2482
- let path = source;
2483
- try {
2484
- path = new URL(source).pathname;
2485
- } catch {
2486
- const noQuery = source.split("?")[0] ?? source;
2487
- path = noQuery.split("#")[0] ?? noQuery;
2235
+ }
2236
+ /**
2237
+ * Detect MIME type from source URL.
2238
+ * @private
2239
+ */
2240
+ detectMimeType(source) {
2241
+ let path = source;
2242
+ try {
2243
+ path = new URL(source).pathname;
2244
+ } catch {
2245
+ const noQuery = source.split("?")[0] ?? source;
2246
+ path = noQuery.split("#")[0] ?? noQuery;
2247
+ }
2248
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
2249
+ switch (ext) {
2250
+ case "m3u8":
2251
+ return "application/x-mpegURL";
2252
+ case "mpd":
2253
+ return "application/dash+xml";
2254
+ case "mp4":
2255
+ case "m4v":
2256
+ return "video/mp4";
2257
+ case "webm":
2258
+ return "video/webm";
2259
+ case "ogg":
2260
+ case "ogv":
2261
+ return "video/ogg";
2262
+ case "mov":
2263
+ return "video/quicktime";
2264
+ case "mkv":
2265
+ return "video/x-matroska";
2266
+ case "mp3":
2267
+ return "audio/mpeg";
2268
+ case "wav":
2269
+ return "audio/wav";
2270
+ case "flac":
2271
+ return "audio/flac";
2272
+ case "aac":
2273
+ case "m4a":
2274
+ return "audio/mp4";
2275
+ default:
2276
+ return "video/mp4";
2277
+ }
2278
+ }
2279
+ }
2280
+ async function createPlayer(options) {
2281
+ const player = new ScarlettPlayer(options);
2282
+ await player.init();
2283
+ return player;
2284
+ }
2285
+ var __defProp = Object.defineProperty;
2286
+ var __export = (target, all) => {
2287
+ for (var name in all)
2288
+ __defProp(target, name, { get: all[name], enumerable: true });
2289
+ };
2290
+ function formatLevel(level) {
2291
+ if (level.name) {
2292
+ return level.name;
2293
+ }
2294
+ if (level.height) {
2295
+ const standardLabels = {
2296
+ 2160: "4K",
2297
+ 1440: "1440p",
2298
+ 1080: "1080p",
2299
+ 720: "720p",
2300
+ 480: "480p",
2301
+ 360: "360p",
2302
+ 240: "240p",
2303
+ 144: "144p"
2304
+ };
2305
+ const closest = Object.keys(standardLabels).map(Number).sort((a, b) => Math.abs(a - level.height) - Math.abs(b - level.height))[0];
2306
+ if (Math.abs(closest - level.height) <= 20) {
2307
+ return standardLabels[closest];
2308
+ }
2309
+ return `${level.height}p`;
2310
+ }
2311
+ if (level.bitrate) {
2312
+ return formatBitrate(level.bitrate);
2313
+ }
2314
+ return "Unknown";
2315
+ }
2316
+ function formatBitrate(bitrate) {
2317
+ if (bitrate >= 1e6) {
2318
+ return `${(bitrate / 1e6).toFixed(1)} Mbps`;
2319
+ }
2320
+ if (bitrate >= 1e3) {
2321
+ return `${Math.round(bitrate / 1e3)} Kbps`;
2322
+ }
2323
+ return `${bitrate} bps`;
2324
+ }
2325
+ function mapLevels(levels, _currentLevel) {
2326
+ return levels.map((level, index) => ({
2327
+ index,
2328
+ width: level.width || 0,
2329
+ height: level.height || 0,
2330
+ bitrate: level.bitrate || 0,
2331
+ label: formatLevel(level),
2332
+ codec: level.codecSet
2333
+ }));
2334
+ }
2335
+ function getInitialBandwidthEstimate(overrideBps) {
2336
+ const HLS_DEFAULT_ESTIMATE = 5e5;
2337
+ if (overrideBps !== void 0 && overrideBps > 0) {
2338
+ return overrideBps;
2339
+ }
2340
+ const connection = navigator.connection;
2341
+ if (connection?.downlink && connection.downlink > 0) {
2342
+ const bps = connection.downlink * 1e6;
2343
+ return Math.round(bps * 0.85);
2344
+ }
2345
+ return HLS_DEFAULT_ESTIMATE;
2346
+ }
2347
+ var HLS_ERROR_TYPES = {
2348
+ NETWORK_ERROR: "networkError",
2349
+ MEDIA_ERROR: "mediaError",
2350
+ MUX_ERROR: "muxError"
2351
+ };
2352
+ function mapErrorType(hlsType) {
2353
+ switch (hlsType) {
2354
+ case HLS_ERROR_TYPES.NETWORK_ERROR:
2355
+ return "network";
2356
+ case HLS_ERROR_TYPES.MEDIA_ERROR:
2357
+ return "media";
2358
+ case HLS_ERROR_TYPES.MUX_ERROR:
2359
+ return "mux";
2360
+ default:
2361
+ return "other";
2362
+ }
2363
+ }
2364
+ function parseHlsError(data) {
2365
+ return {
2366
+ type: mapErrorType(data.type),
2367
+ details: data.details || "Unknown error",
2368
+ fatal: data.fatal || false,
2369
+ url: data.url,
2370
+ reason: data.reason,
2371
+ response: data.response
2372
+ };
2373
+ }
2374
+ function setupHlsEventHandlers(hls, api, callbacks) {
2375
+ const handlers = [];
2376
+ const addHandler = (event, handler) => {
2377
+ hls.on(event, handler);
2378
+ handlers.push({ event, handler });
2379
+ };
2380
+ addHandler("hlsManifestParsed", (_event, data) => {
2381
+ api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
2382
+ const levels = data.levels.map((level, index) => ({
2383
+ id: `level-${index}`,
2384
+ label: formatLevel(level),
2385
+ width: level.width,
2386
+ height: level.height,
2387
+ bitrate: level.bitrate,
2388
+ active: index === hls.currentLevel
2389
+ }));
2390
+ api.setState("qualities", levels);
2391
+ api.emit("quality:levels", {
2392
+ levels: levels.map((l) => ({ id: l.id, label: l.label }))
2393
+ });
2394
+ callbacks.onManifestParsed?.(data.levels);
2395
+ });
2396
+ addHandler("hlsLevelSwitched", (_event, data) => {
2397
+ const level = hls.levels[data.level];
2398
+ const isAuto = callbacks.getIsAutoQuality?.() ?? hls.autoLevelEnabled;
2399
+ api.logger.debug("HLS level switched", { level: data.level, height: level?.height, auto: isAuto });
2400
+ if (level) {
2401
+ const label = isAuto ? `Auto (${formatLevel(level)})` : formatLevel(level);
2402
+ api.setState("currentQuality", {
2403
+ id: isAuto ? "auto" : `level-${data.level}`,
2404
+ label,
2405
+ width: level.width,
2406
+ height: level.height,
2407
+ bitrate: level.bitrate,
2408
+ active: true
2409
+ });
2410
+ }
2411
+ api.emit("quality:change", {
2412
+ quality: level ? formatLevel(level) : "auto",
2413
+ auto: isAuto
2414
+ });
2415
+ callbacks.onLevelSwitched?.(data.level);
2416
+ });
2417
+ let lastBandwidthUpdate = 0;
2418
+ addHandler("hlsFragLoaded", () => {
2419
+ const now = Date.now();
2420
+ if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
2421
+ lastBandwidthUpdate = now;
2422
+ api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
2423
+ }
2424
+ callbacks.onFragLoaded?.();
2425
+ });
2426
+ addHandler("hlsFragBuffered", () => {
2427
+ api.setState("buffering", false);
2428
+ callbacks.onBufferUpdate?.();
2429
+ });
2430
+ addHandler("hlsFragLoading", () => {
2431
+ api.setState("buffering", true);
2432
+ });
2433
+ addHandler("hlsLevelLoaded", (_event, data) => {
2434
+ if (data.details?.live !== void 0) {
2435
+ api.setState("live", data.details.live);
2436
+ if (data.details.live) {
2437
+ const video = hls.media;
2438
+ if (video && video.seekable && video.seekable.length > 0) {
2439
+ const start = video.seekable.start(0);
2440
+ const end = video.seekable.end(video.seekable.length - 1);
2441
+ api.setState("seekableRange", { start, end });
2442
+ const threshold = (data.details.targetduration ?? 3) * 3;
2443
+ const isAtLiveEdge = end - video.currentTime < threshold;
2444
+ api.setState("liveEdge", isAtLiveEdge);
2445
+ const latency = end - video.currentTime;
2446
+ api.setState("liveLatency", Math.max(0, latency));
2447
+ }
2448
+ }
2449
+ callbacks.onLiveUpdate?.();
2450
+ }
2451
+ });
2452
+ addHandler("hlsError", (_event, data) => {
2453
+ const error = parseHlsError(data);
2454
+ const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
2455
+ if (isBufferHoleSeek) {
2456
+ api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
2457
+ details: error.details,
2458
+ reason: error.reason
2459
+ });
2460
+ } else if (error.fatal) {
2461
+ api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
2462
+ type: error.type,
2463
+ details: error.details,
2464
+ url: error.url
2465
+ });
2466
+ } else {
2467
+ api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
2468
+ type: error.type,
2469
+ details: error.details,
2470
+ fatal: error.fatal,
2471
+ url: error.url
2472
+ });
2473
+ }
2474
+ callbacks.onError?.(error);
2475
+ });
2476
+ return () => {
2477
+ for (const { event, handler } of handlers) {
2478
+ hls.off(event, handler);
2479
+ }
2480
+ handlers.length = 0;
2481
+ };
2482
+ }
2483
+ function setupVideoEventHandlers(video, api) {
2484
+ const handlers = [];
2485
+ const addHandler = (event, handler) => {
2486
+ video.addEventListener(event, handler);
2487
+ handlers.push({ event, handler });
2488
+ };
2489
+ addHandler("play", () => {
2490
+ api.setState("paused", false);
2491
+ });
2492
+ addHandler("playing", () => {
2493
+ api.setState("playing", true);
2494
+ api.setState("paused", false);
2495
+ api.setState("waiting", false);
2496
+ api.setState("buffering", false);
2497
+ api.setState("playbackState", "playing");
2498
+ });
2499
+ addHandler("pause", () => {
2500
+ api.setState("playing", false);
2501
+ api.setState("paused", true);
2502
+ api.setState("playbackState", "paused");
2503
+ });
2504
+ addHandler("ended", () => {
2505
+ api.setState("playing", false);
2506
+ api.setState("ended", true);
2507
+ api.setState("playbackState", "ended");
2508
+ api.emit("playback:ended", void 0);
2509
+ });
2510
+ addHandler("timeupdate", () => {
2511
+ api.setState("currentTime", video.currentTime);
2512
+ api.emit("playback:timeupdate", { currentTime: video.currentTime });
2513
+ const isLive = api.getState("live");
2514
+ if (isLive && video.seekable && video.seekable.length > 0) {
2515
+ const start = video.seekable.start(0);
2516
+ const end = video.seekable.end(video.seekable.length - 1);
2517
+ api.setState("seekableRange", { start, end });
2518
+ const isAtLiveEdge = end - video.currentTime < 10;
2519
+ api.setState("liveEdge", isAtLiveEdge);
2520
+ api.setState("liveLatency", Math.max(0, end - video.currentTime));
2521
+ }
2522
+ });
2523
+ addHandler("durationchange", () => {
2524
+ api.setState("duration", video.duration || 0);
2525
+ api.emit("media:loadedmetadata", { duration: video.duration || 0 });
2526
+ });
2527
+ addHandler("waiting", () => {
2528
+ api.setState("waiting", true);
2529
+ api.setState("buffering", true);
2530
+ api.emit("media:waiting", void 0);
2531
+ });
2532
+ addHandler("canplay", () => {
2533
+ api.setState("waiting", false);
2534
+ api.setState("playbackState", "ready");
2535
+ api.emit("media:canplay", void 0);
2536
+ });
2537
+ addHandler("canplaythrough", () => {
2538
+ api.setState("buffering", false);
2539
+ api.emit("media:canplaythrough", void 0);
2540
+ });
2541
+ addHandler("progress", () => {
2542
+ if (video.buffered.length > 0) {
2543
+ const bufferedEnd = video.buffered.end(video.buffered.length - 1);
2544
+ const bufferedAmount = video.duration > 0 ? bufferedEnd / video.duration : 0;
2545
+ api.setState("bufferedAmount", bufferedAmount);
2546
+ api.setState("buffered", video.buffered);
2547
+ api.emit("media:progress", { buffered: bufferedAmount });
2548
+ }
2549
+ });
2550
+ addHandler("seeking", () => {
2551
+ api.setState("seeking", true);
2552
+ });
2553
+ addHandler("seeked", () => {
2554
+ api.setState("seeking", false);
2555
+ api.emit("playback:seeked", { time: video.currentTime });
2556
+ });
2557
+ addHandler("volumechange", () => {
2558
+ api.setState("volume", video.volume);
2559
+ api.setState("muted", video.muted);
2560
+ api.emit("volume:change", { volume: video.volume, muted: video.muted });
2561
+ });
2562
+ addHandler("ratechange", () => {
2563
+ api.setState("playbackRate", video.playbackRate);
2564
+ api.emit("playback:ratechange", { rate: video.playbackRate });
2565
+ });
2566
+ addHandler("loadedmetadata", () => {
2567
+ api.setState("duration", video.duration);
2568
+ api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
2569
+ });
2570
+ addHandler("loadeddata", () => {
2571
+ if (video.videoWidth > 0) {
2572
+ api.setState("mediaType", "video");
2488
2573
  }
2489
- const ext = path.split(".").pop()?.toLowerCase() ?? "";
2490
- switch (ext) {
2491
- case "m3u8":
2492
- return "application/x-mpegURL";
2493
- case "mpd":
2494
- return "application/dash+xml";
2495
- case "mp4":
2496
- case "m4v":
2497
- return "video/mp4";
2498
- case "webm":
2499
- return "video/webm";
2500
- case "ogg":
2501
- case "ogv":
2502
- return "video/ogg";
2503
- case "mov":
2504
- return "video/quicktime";
2505
- case "mkv":
2506
- return "video/x-matroska";
2507
- case "mp3":
2508
- return "audio/mpeg";
2509
- case "wav":
2510
- return "audio/wav";
2511
- case "flac":
2512
- return "audio/flac";
2513
- case "aac":
2514
- case "m4a":
2515
- return "audio/mp4";
2516
- default:
2517
- return "video/mp4";
2574
+ });
2575
+ addHandler("error", () => {
2576
+ const error = video.error;
2577
+ if (error) {
2578
+ api.logger.error("Video element error", { code: error.code, message: error.message });
2579
+ api.emit("media:error", { error: new Error(error.message || "Video playback error") });
2518
2580
  }
2519
- }
2520
- }
2521
- async function createPlayer(options) {
2522
- const player = new ScarlettPlayer(options);
2523
- await player.init();
2524
- return player;
2525
- }
2526
- var hlsConstructor = null;
2527
- var loadingPromise = null;
2528
- function supportsNativeHLS() {
2529
- if (typeof document === "undefined") return false;
2530
- const video = document.createElement("video");
2531
- return video.canPlayType("application/vnd.apple.mpegurl") !== "";
2532
- }
2533
- function isHlsJsSupported() {
2534
- if (hlsConstructor) {
2535
- return hlsConstructor.isSupported();
2536
- }
2537
- if (typeof window === "undefined") return false;
2538
- return !!(window.MediaSource || window.WebKitMediaSource);
2539
- }
2540
- function isHLSSupported() {
2541
- return supportsNativeHLS() || isHlsJsSupported();
2542
- }
2543
- async function loadHlsJs() {
2544
- if (hlsConstructor) {
2545
- return hlsConstructor;
2546
- }
2547
- if (loadingPromise) {
2548
- return loadingPromise;
2549
- }
2550
- loadingPromise = (async () => {
2551
- try {
2552
- const hlsModule = await import("./hls.js");
2553
- hlsConstructor = hlsModule.default;
2554
- if (!hlsConstructor.isSupported()) {
2555
- throw new Error("hls.js is not supported in this browser");
2581
+ });
2582
+ addHandler("enterpictureinpicture", () => {
2583
+ api.setState("pip", true);
2584
+ api.logger.debug("PiP: entered (standard)");
2585
+ });
2586
+ addHandler("leavepictureinpicture", () => {
2587
+ api.setState("pip", false);
2588
+ api.logger.debug("PiP: exited (standard)");
2589
+ if (!video.paused || api.getState("playing")) {
2590
+ video.play().catch(() => {
2591
+ });
2592
+ }
2593
+ });
2594
+ const webkitVideo = video;
2595
+ if ("webkitPresentationMode" in video) {
2596
+ addHandler("webkitpresentationmodechanged", () => {
2597
+ const mode = webkitVideo.webkitPresentationMode;
2598
+ const isInPip = mode === "picture-in-picture";
2599
+ api.setState("pip", isInPip);
2600
+ api.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
2601
+ if (mode === "inline" && video.paused) {
2602
+ video.play().catch(() => {
2603
+ });
2556
2604
  }
2557
- return hlsConstructor;
2558
- } catch (error) {
2559
- loadingPromise = null;
2560
- throw new Error(
2561
- `Failed to load hls.js: ${error instanceof Error ? error.message : "Unknown error"}`
2562
- );
2605
+ });
2606
+ }
2607
+ return () => {
2608
+ for (const { event, handler } of handlers) {
2609
+ video.removeEventListener(event, handler);
2563
2610
  }
2564
- })();
2565
- return loadingPromise;
2611
+ handlers.length = 0;
2612
+ };
2566
2613
  }
2567
- function createHlsInstance(config) {
2568
- if (!hlsConstructor) {
2569
- throw new Error("hls.js is not loaded. Call loadHlsJs() first.");
2570
- }
2571
- return new hlsConstructor(config);
2614
+ var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
2615
+ var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
2616
+ function isValidPlaylistDocument(data, contextType) {
2617
+ if (typeof data !== "string" || data.length === 0) return false;
2618
+ const text = data.trimStart();
2619
+ if (!text.startsWith("#EXTM3U")) return false;
2620
+ if (contextType && MEDIA_PLAYLIST_CONTEXTS.includes(contextType)) {
2621
+ return /^#EXT(?:INF|-X-TARGETDURATION):/m.test(text);
2622
+ }
2623
+ return true;
2572
2624
  }
2573
- function getHlsConstructor() {
2574
- return hlsConstructor;
2625
+ function createValidatingPlaylistLoader(Hls) {
2626
+ const BaseLoader = Hls.DefaultConfig.loader;
2627
+ return class ValidatingPlaylistLoader extends BaseLoader {
2628
+ /**
2629
+ * Load a playlist, validating the response document before it reaches
2630
+ * the M3U8 parser.
2631
+ *
2632
+ * @param context - hls.js loader context
2633
+ * @param config - hls.js loader config
2634
+ * @param callbacks - hls.js loader callbacks
2635
+ */
2636
+ load(context, config, callbacks) {
2637
+ const wrapped = {
2638
+ ...callbacks,
2639
+ onSuccess: (response, stats, ctx, networkDetails) => {
2640
+ if (!isValidPlaylistDocument(response?.data, ctx?.type)) {
2641
+ callbacks.onError(
2642
+ { code: 0, text: PLAYLIST_INVALID_TEXT },
2643
+ ctx,
2644
+ networkDetails,
2645
+ stats
2646
+ );
2647
+ return;
2648
+ }
2649
+ callbacks.onSuccess(response, stats, ctx, networkDetails);
2650
+ }
2651
+ };
2652
+ super.load(context, config, wrapped);
2653
+ }
2654
+ };
2575
2655
  }
2576
2656
  var DEFAULT_CONFIG$3 = {
2577
2657
  debug: false,
@@ -2594,14 +2674,16 @@ var DEFAULT_CONFIG$3 = {
2594
2674
  autoReconnect: true,
2595
2675
  reconnectBaseDelayMs: 2e3,
2596
2676
  reconnectMaxDelayMs: 3e4,
2597
- reconnectWindowMs: 3e5
2677
+ reconnectWindowMs: 3e5,
2678
+ // Never index a malformed live playlist refresh blindly
2679
+ validatePlaylists: true
2598
2680
  };
2599
2681
  var MANIFEST_PHASE_ERRORS = [
2600
2682
  "manifestLoadError",
2601
2683
  "manifestLoadTimeOut",
2602
2684
  "manifestParsingError"
2603
2685
  ];
2604
- function createHLSPlugin(config) {
2686
+ function createHLSPluginWith(loader, variant, config) {
2605
2687
  const mergedConfig = { ...DEFAULT_CONFIG$3, ...config };
2606
2688
  let api = null;
2607
2689
  let hls = null;
@@ -2611,6 +2693,8 @@ function createHLSPlugin(config) {
2611
2693
  let cleanupHlsEvents = null;
2612
2694
  let cleanupVideoEvents = null;
2613
2695
  let isAutoQuality = true;
2696
+ let loadSession = 0;
2697
+ let abortPendingLoad = null;
2614
2698
  let networkRetryCount = 0;
2615
2699
  let mediaRetryCount = 0;
2616
2700
  let retryTimeout = null;
@@ -2643,7 +2727,9 @@ function createHLSPlugin(config) {
2643
2727
  api?.container.appendChild(video);
2644
2728
  return video;
2645
2729
  };
2646
- const cleanup = () => {
2730
+ const teardownPipeline = (reason) => {
2731
+ abortPendingLoad?.(reason ?? new Error("HLS load cancelled"));
2732
+ abortPendingLoad = null;
2647
2733
  cleanupHlsEvents?.();
2648
2734
  cleanupHlsEvents = null;
2649
2735
  cleanupVideoEvents?.();
@@ -2656,6 +2742,9 @@ function createHLSPlugin(config) {
2656
2742
  hls.destroy();
2657
2743
  hls = null;
2658
2744
  }
2745
+ };
2746
+ const cleanup = (reason) => {
2747
+ teardownPipeline(reason);
2659
2748
  currentSrc = null;
2660
2749
  isNative = false;
2661
2750
  isAutoQuality = true;
@@ -2664,7 +2753,17 @@ function createHLSPlugin(config) {
2664
2753
  errorCount = 0;
2665
2754
  errorWindowStart = 0;
2666
2755
  };
2667
- const buildHlsConfig = () => ({
2756
+ const buildHlsConfig = () => {
2757
+ const config2 = buildBaseHlsConfig();
2758
+ if (mergedConfig.validatePlaylists !== false) {
2759
+ const Hls = loader.getHlsConstructor();
2760
+ if (Hls && Hls.DefaultConfig?.loader) {
2761
+ config2.pLoader = createValidatingPlaylistLoader(Hls);
2762
+ }
2763
+ }
2764
+ return config2;
2765
+ };
2766
+ const buildBaseHlsConfig = () => ({
2668
2767
  debug: mergedConfig.debug,
2669
2768
  autoStartLoad: mergedConfig.autoStartLoad,
2670
2769
  startPosition: mergedConfig.startPosition,
@@ -2692,7 +2791,21 @@ function createHLSPlugin(config) {
2692
2791
  const jitter = delay * (0.7 + Math.random() * 0.3);
2693
2792
  return jitter;
2694
2793
  };
2794
+ const APPEND_ERROR_DETAILS = [
2795
+ "bufferAppendError",
2796
+ "bufferAppendingError",
2797
+ "bufferAddCodecError"
2798
+ ];
2695
2799
  const mapFatalErrorCode = (error) => {
2800
+ if (error.response?.text === PLAYLIST_INVALID_TEXT) {
2801
+ return ErrorCode.PLAYLIST_INVALID;
2802
+ }
2803
+ if (error.details === "bufferFullError") {
2804
+ return ErrorCode.MEDIA_BUFFER_FULL;
2805
+ }
2806
+ if (APPEND_ERROR_DETAILS.includes(error.details)) {
2807
+ return ErrorCode.MEDIA_APPEND_ERROR;
2808
+ }
2696
2809
  switch (error.type) {
2697
2810
  case "network":
2698
2811
  return ErrorCode.MEDIA_NETWORK_ERROR;
@@ -2717,7 +2830,7 @@ function createHLSPlugin(config) {
2717
2830
  maybeScheduleReconnect(error);
2718
2831
  };
2719
2832
  const handleHlsError = (error) => {
2720
- const Hls = getHlsConstructor();
2833
+ const Hls = loader.getHlsConstructor();
2721
2834
  if (!Hls || !hls) return false;
2722
2835
  const now = Date.now();
2723
2836
  if (now - errorWindowStart > ERROR_WINDOW_MS) {
@@ -2729,10 +2842,7 @@ function createHLSPlugin(config) {
2729
2842
  if (errorCount >= MAX_ERRORS_IN_WINDOW) {
2730
2843
  api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
2731
2844
  emitFatalError(error, true);
2732
- cleanupHlsEvents?.();
2733
- cleanupHlsEvents = null;
2734
- hls.destroy();
2735
- hls = null;
2845
+ teardownPipeline(new Error(error.details));
2736
2846
  return true;
2737
2847
  }
2738
2848
  if (error.fatal) {
@@ -2753,8 +2863,9 @@ function createHLSPlugin(config) {
2753
2863
  clearTimeout(retryTimeout);
2754
2864
  }
2755
2865
  const isManifestPhase = MANIFEST_PHASE_ERRORS.includes(error.details);
2866
+ const retry_session = loadSession;
2756
2867
  retryTimeout = setTimeout(() => {
2757
- if (!hls) return;
2868
+ if (retry_session !== loadSession || !hls) return;
2758
2869
  if (isManifestPhase && currentSrc) {
2759
2870
  hls.loadSource(currentSrc);
2760
2871
  } else {
@@ -2777,10 +2888,10 @@ function createHLSPlugin(config) {
2777
2888
  if (retryTimeout) {
2778
2889
  clearTimeout(retryTimeout);
2779
2890
  }
2891
+ const retry_session = loadSession;
2780
2892
  retryTimeout = setTimeout(() => {
2781
- if (hls) {
2782
- hls.recoverMediaError();
2783
- }
2893
+ if (retry_session !== loadSession || !hls) return;
2894
+ hls.recoverMediaError();
2784
2895
  }, delay);
2785
2896
  break;
2786
2897
  }
@@ -2792,6 +2903,7 @@ function createHLSPlugin(config) {
2792
2903
  return false;
2793
2904
  };
2794
2905
  const loadNative = async (src) => {
2906
+ const session = loadSession;
2795
2907
  const videoEl = getOrCreateVideo();
2796
2908
  isNative = true;
2797
2909
  if (api) {
@@ -2799,7 +2911,12 @@ function createHLSPlugin(config) {
2799
2911
  }
2800
2912
  return new Promise((resolve, reject) => {
2801
2913
  let watchdog = null;
2914
+ let settled = false;
2802
2915
  const settle = () => {
2916
+ settled = true;
2917
+ if (abortPendingLoad === abort) {
2918
+ abortPendingLoad = null;
2919
+ }
2803
2920
  videoEl.removeEventListener("loadedmetadata", onLoaded);
2804
2921
  videoEl.removeEventListener("error", onError);
2805
2922
  if (watchdog !== null) {
@@ -2807,7 +2924,19 @@ function createHLSPlugin(config) {
2807
2924
  watchdog = null;
2808
2925
  }
2809
2926
  };
2927
+ const abort = (reason) => {
2928
+ if (settled) return;
2929
+ settle();
2930
+ reject(reason);
2931
+ };
2932
+ abortPendingLoad = abort;
2810
2933
  const onLoaded = () => {
2934
+ if (settled) return;
2935
+ if (session !== loadSession) {
2936
+ settle();
2937
+ reject(new Error("HLS load cancelled"));
2938
+ return;
2939
+ }
2811
2940
  settle();
2812
2941
  hasPlayedContent = true;
2813
2942
  const onFatalVideoError = () => {
@@ -2830,6 +2959,7 @@ function createHLSPlugin(config) {
2830
2959
  resolve();
2831
2960
  };
2832
2961
  const onError = () => {
2962
+ if (settled) return;
2833
2963
  settle();
2834
2964
  const error = videoEl.error;
2835
2965
  reject(new Error(error?.message || "Failed to load HLS source"));
@@ -2837,6 +2967,7 @@ function createHLSPlugin(config) {
2837
2967
  const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
2838
2968
  if (timeout_ms > 0) {
2839
2969
  watchdog = setTimeout(() => {
2970
+ if (settled || session !== loadSession) return;
2840
2971
  settle();
2841
2972
  reject(new Error("Video took too long to load (network timeout)"));
2842
2973
  }, timeout_ms);
@@ -2848,10 +2979,14 @@ function createHLSPlugin(config) {
2848
2979
  });
2849
2980
  };
2850
2981
  const loadWithHlsJs = async (src) => {
2851
- await loadHlsJs();
2982
+ const session = loadSession;
2983
+ await loader.loadHlsJs();
2984
+ if (session !== loadSession) {
2985
+ throw new Error("HLS load cancelled");
2986
+ }
2852
2987
  const videoEl = getOrCreateVideo();
2853
2988
  isNative = false;
2854
- hls = createHlsInstance(buildHlsConfig());
2989
+ hls = loader.createHlsInstance(buildHlsConfig());
2855
2990
  if (api) {
2856
2991
  cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
2857
2992
  }
@@ -2868,10 +3003,24 @@ function createHLSPlugin(config) {
2868
3003
  watchdog = null;
2869
3004
  }
2870
3005
  };
3006
+ const abort = (reason) => {
3007
+ if (resolved) return;
3008
+ resolved = true;
3009
+ clearWatchdog();
3010
+ reject(reason);
3011
+ };
3012
+ abortPendingLoad = abort;
3013
+ const releaseAbort = () => {
3014
+ if (abortPendingLoad === abort) {
3015
+ abortPendingLoad = null;
3016
+ }
3017
+ };
2871
3018
  cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
2872
3019
  onManifestParsed: () => {
3020
+ if (session !== loadSession) return;
2873
3021
  if (!resolved) {
2874
3022
  resolved = true;
3023
+ releaseAbort();
2875
3024
  clearWatchdog();
2876
3025
  hasPlayedContent = true;
2877
3026
  api?.setState("source", { src, type: "application/x-mpegURL" });
@@ -2882,14 +3031,17 @@ function createHLSPlugin(config) {
2882
3031
  onLevelSwitched: () => {
2883
3032
  },
2884
3033
  onError: (error) => {
3034
+ if (session !== loadSession) return;
2885
3035
  const terminal = handleHlsError(error);
2886
3036
  if (terminal && !resolved) {
2887
3037
  resolved = true;
3038
+ releaseAbort();
2888
3039
  clearWatchdog();
2889
3040
  reject(new Error(error.details));
2890
3041
  }
2891
3042
  },
2892
3043
  onFragLoaded: () => {
3044
+ if (session !== loadSession) return;
2893
3045
  if (networkRetryCount > 0 || mediaRetryCount > 0) {
2894
3046
  api?.logger.debug("Playback recovered, resetting retry budgets");
2895
3047
  networkRetryCount = 0;
@@ -2901,17 +3053,11 @@ function createHLSPlugin(config) {
2901
3053
  const timeout_ms = mergedConfig.loadTimeoutMs ?? 3e4;
2902
3054
  if (timeout_ms > 0) {
2903
3055
  watchdog = setTimeout(() => {
2904
- if (resolved) return;
3056
+ if (resolved || session !== loadSession) return;
2905
3057
  resolved = true;
3058
+ releaseAbort();
2906
3059
  api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
2907
- if (retryTimeout) {
2908
- clearTimeout(retryTimeout);
2909
- retryTimeout = null;
2910
- }
2911
- cleanupHlsEvents?.();
2912
- cleanupHlsEvents = null;
2913
- hls?.destroy();
2914
- hls = null;
3060
+ teardownPipeline();
2915
3061
  reject(new Error("Video took too long to load (network timeout)"));
2916
3062
  }, timeout_ms);
2917
3063
  }
@@ -2958,6 +3104,7 @@ function createHLSPlugin(config) {
2958
3104
  };
2959
3105
  const attemptReconnect = async () => {
2960
3106
  if (!api || !currentSrc) return;
3107
+ const session = ++loadSession;
2961
3108
  reconnectAttempts++;
2962
3109
  const saved_src = currentSrc;
2963
3110
  const was_live = api.getState("live");
@@ -2965,27 +3112,19 @@ function createHLSPlugin(config) {
2965
3112
  const resume_position = reconnectResumePosition;
2966
3113
  api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
2967
3114
  try {
2968
- cleanupHlsEvents?.();
2969
- cleanupHlsEvents = null;
2970
- cleanupVideoEvents?.();
2971
- cleanupVideoEvents = null;
2972
- if (retryTimeout) {
2973
- clearTimeout(retryTimeout);
2974
- retryTimeout = null;
2975
- }
2976
- hls?.destroy();
2977
- hls = null;
3115
+ teardownPipeline(new Error("HLS load cancelled: reconnecting"));
2978
3116
  networkRetryCount = 0;
2979
3117
  mediaRetryCount = 0;
2980
3118
  errorCount = 0;
2981
3119
  errorWindowStart = 0;
2982
3120
  currentSrc = saved_src;
2983
3121
  api.setState("playbackState", "loading");
2984
- if (was_native && supportsNativeHLS()) {
3122
+ if (was_native && loader.supportsNativeHLS()) {
2985
3123
  await loadNative(saved_src);
2986
3124
  } else {
2987
3125
  await loadWithHlsJs(saved_src);
2988
3126
  }
3127
+ if (session !== loadSession) return;
2989
3128
  if (!was_live && video && resume_position > 0) {
2990
3129
  video.currentTime = resume_position;
2991
3130
  }
@@ -2999,18 +3138,19 @@ function createHLSPlugin(config) {
2999
3138
  } catch {
3000
3139
  }
3001
3140
  } catch {
3141
+ if (session !== loadSession) return;
3002
3142
  api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
3003
3143
  scheduleReconnectAttempt();
3004
3144
  }
3005
3145
  };
3006
3146
  const plugin = {
3007
3147
  id: "hls-provider",
3008
- name: "HLS Provider",
3148
+ name: variant.name,
3009
3149
  version: "1.0.0",
3010
3150
  type: "provider",
3011
- description: "HLS playback provider using hls.js",
3151
+ description: variant.description,
3012
3152
  canPlay(src) {
3013
- if (!isHLSSupported()) return false;
3153
+ if (!loader.isHLSSupported()) return false;
3014
3154
  const url = src.toLowerCase();
3015
3155
  const urlWithoutQuery = url.split("?")[0].split("#")[0];
3016
3156
  if (urlWithoutQuery.endsWith(".m3u8")) return true;
@@ -3020,7 +3160,7 @@ function createHLSPlugin(config) {
3020
3160
  },
3021
3161
  async init(pluginApi) {
3022
3162
  api = pluginApi;
3023
- api.logger.info("HLS plugin initialized");
3163
+ api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
3024
3164
  const unsubPlay = api.on("playback:play", async () => {
3025
3165
  if (!video) return;
3026
3166
  try {
@@ -3108,13 +3248,14 @@ function createHLSPlugin(config) {
3108
3248
  });
3109
3249
  },
3110
3250
  async destroy() {
3111
- api?.logger.info("HLS plugin destroying");
3251
+ api?.logger.info(`HLS plugin${variant.logSuffix} destroying`);
3252
+ loadSession++;
3112
3253
  cancelReconnect();
3113
3254
  if (onlineListener && typeof window !== "undefined") {
3114
3255
  window.removeEventListener("online", onlineListener);
3115
3256
  onlineListener = null;
3116
3257
  }
3117
- cleanup();
3258
+ cleanup(new Error("HLS load cancelled: player destroyed"));
3118
3259
  if (video?.parentNode) {
3119
3260
  video.parentNode.removeChild(video);
3120
3261
  }
@@ -3123,25 +3264,27 @@ function createHLSPlugin(config) {
3123
3264
  },
3124
3265
  async loadSource(src) {
3125
3266
  if (!api) throw new Error("Plugin not initialized");
3126
- api.logger.info("Loading HLS source", { src });
3267
+ api.logger.info(`Loading HLS source${variant.logSuffix}`, { src });
3268
+ const session = ++loadSession;
3127
3269
  cancelReconnect();
3128
3270
  hasPlayedContent = false;
3129
- cleanup();
3271
+ cleanup(new Error("HLS load cancelled: superseded by a new load"));
3130
3272
  currentSrc = src;
3131
3273
  api.setState("playbackState", "loading");
3132
3274
  api.setState("buffering", true);
3133
- if (api.getState("airplayActive") && supportsNativeHLS()) {
3275
+ if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
3134
3276
  api.logger.info("Using native HLS (AirPlay active)");
3135
3277
  await loadNative(src);
3136
- } else if (isHlsJsSupported()) {
3137
- api.logger.info("Using hls.js for HLS playback");
3278
+ } else if (loader.isHlsJsSupported()) {
3279
+ api.logger.info(`Using ${variant.engineLabel} for HLS playback`);
3138
3280
  await loadWithHlsJs(src);
3139
- } else if (supportsNativeHLS()) {
3281
+ } else if (loader.supportsNativeHLS()) {
3140
3282
  api.logger.info("Using native HLS playback (hls.js not supported)");
3141
3283
  await loadNative(src);
3142
3284
  } else {
3143
3285
  throw new Error("HLS playback not supported in this browser");
3144
3286
  }
3287
+ if (session !== loadSession) return;
3145
3288
  if (video) {
3146
3289
  const muted = api.getState("muted");
3147
3290
  const volume = api.getState("volume");
@@ -3193,7 +3336,7 @@ function createHLSPlugin(config) {
3193
3336
  api?.logger.debug("Already using native HLS");
3194
3337
  return;
3195
3338
  }
3196
- if (!supportsNativeHLS()) {
3339
+ if (!loader.supportsNativeHLS()) {
3197
3340
  api?.logger.warn("Native HLS not supported in this browser");
3198
3341
  return;
3199
3342
  }
@@ -3205,8 +3348,10 @@ function createHLSPlugin(config) {
3205
3348
  const wasPlaying = api?.getState("playing") || false;
3206
3349
  const currentTime = video?.currentTime || 0;
3207
3350
  const savedSrc = currentSrc;
3208
- cleanup();
3351
+ const session = ++loadSession;
3352
+ cleanup(new Error("HLS load cancelled: switching to native HLS"));
3209
3353
  await loadNative(savedSrc);
3354
+ if (session !== loadSession) return;
3210
3355
  if (video && currentTime > 0) {
3211
3356
  video.currentTime = currentTime;
3212
3357
  }
@@ -3228,7 +3373,7 @@ function createHLSPlugin(config) {
3228
3373
  api?.logger.debug("Already using hls.js");
3229
3374
  return;
3230
3375
  }
3231
- if (!isHlsJsSupported()) {
3376
+ if (!loader.isHlsJsSupported()) {
3232
3377
  api?.logger.warn("hls.js not supported in this browser");
3233
3378
  return;
3234
3379
  }
@@ -3240,8 +3385,10 @@ function createHLSPlugin(config) {
3240
3385
  const wasPlaying = api?.getState("playing") || false;
3241
3386
  const currentTime = video?.currentTime || 0;
3242
3387
  const savedSrc = currentSrc;
3243
- cleanup();
3388
+ const session = ++loadSession;
3389
+ cleanup(new Error("HLS load cancelled: switching to hls.js"));
3244
3390
  await loadWithHlsJs(savedSrc);
3391
+ if (session !== loadSession) return;
3245
3392
  if (video && currentTime > 0) {
3246
3393
  video.currentTime = currentTime;
3247
3394
  }
@@ -3257,6 +3404,90 @@ function createHLSPlugin(config) {
3257
3404
  };
3258
3405
  return plugin;
3259
3406
  }
3407
+ var hls_loader_exports = {};
3408
+ __export(hls_loader_exports, {
3409
+ createHlsInstance: () => createHlsInstance,
3410
+ getHlsConstructor: () => getHlsConstructor,
3411
+ isHLSSupported: () => isHLSSupported,
3412
+ isHlsJsSupported: () => isHlsJsSupported,
3413
+ loadHlsJs: () => loadHlsJs,
3414
+ resetLoader: () => resetLoader,
3415
+ shouldPreferNativeHLS: () => shouldPreferNativeHLS,
3416
+ supportsNativeHLS: () => supportsNativeHLS
3417
+ });
3418
+ var hlsConstructor = null;
3419
+ var loadingPromise = null;
3420
+ function supportsNativeHLS() {
3421
+ if (typeof document === "undefined") return false;
3422
+ const video = document.createElement("video");
3423
+ return video.canPlayType("application/vnd.apple.mpegurl") !== "";
3424
+ }
3425
+ function shouldPreferNativeHLS() {
3426
+ if (!supportsNativeHLS()) return false;
3427
+ if (typeof navigator === "undefined") return false;
3428
+ const ua = navigator.userAgent;
3429
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua);
3430
+ return isSafari;
3431
+ }
3432
+ function isHlsJsSupported() {
3433
+ if (hlsConstructor) {
3434
+ return hlsConstructor.isSupported();
3435
+ }
3436
+ if (typeof window === "undefined") return false;
3437
+ return !!(window.MediaSource || window.WebKitMediaSource);
3438
+ }
3439
+ function isHLSSupported() {
3440
+ return supportsNativeHLS() || isHlsJsSupported();
3441
+ }
3442
+ async function loadHlsJs() {
3443
+ if (hlsConstructor) {
3444
+ return hlsConstructor;
3445
+ }
3446
+ if (loadingPromise) {
3447
+ return loadingPromise;
3448
+ }
3449
+ loadingPromise = (async () => {
3450
+ try {
3451
+ const hlsModule = await import("./hls.js");
3452
+ hlsConstructor = hlsModule.default;
3453
+ if (!hlsConstructor.isSupported()) {
3454
+ throw new Error("hls.js is not supported in this browser");
3455
+ }
3456
+ return hlsConstructor;
3457
+ } catch (error) {
3458
+ loadingPromise = null;
3459
+ throw new Error(
3460
+ `Failed to load hls.js: ${error instanceof Error ? error.message : "Unknown error"}`
3461
+ );
3462
+ }
3463
+ })();
3464
+ return loadingPromise;
3465
+ }
3466
+ function createHlsInstance(config) {
3467
+ if (!hlsConstructor) {
3468
+ throw new Error("hls.js is not loaded. Call loadHlsJs() first.");
3469
+ }
3470
+ return new hlsConstructor(config);
3471
+ }
3472
+ function getHlsConstructor() {
3473
+ return hlsConstructor;
3474
+ }
3475
+ function resetLoader() {
3476
+ hlsConstructor = null;
3477
+ loadingPromise = null;
3478
+ }
3479
+ function createHLSPlugin(config) {
3480
+ return createHLSPluginWith(
3481
+ hls_loader_exports,
3482
+ {
3483
+ name: "HLS Provider",
3484
+ description: "HLS playback provider using hls.js",
3485
+ logSuffix: "",
3486
+ engineLabel: "hls.js"
3487
+ },
3488
+ config
3489
+ );
3490
+ }
3260
3491
  var DEFAULT_THEME = {
3261
3492
  primary: "#6366f1",
3262
3493
  background: "#18181b",