@scarlett-player/embed 0.1.2 → 0.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.
@@ -0,0 +1,4554 @@
1
+ function formatLevel(level) {
2
+ if (level.name) {
3
+ return level.name;
4
+ }
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];
19
+ }
20
+ return `${level.height}p`;
21
+ }
22
+ if (level.bitrate) {
23
+ return formatBitrate(level.bitrate);
24
+ }
25
+ return "Unknown";
26
+ }
27
+ function formatBitrate(bitrate) {
28
+ if (bitrate >= 1e6) {
29
+ return `${(bitrate / 1e6).toFixed(1)} Mbps`;
30
+ }
31
+ if (bitrate >= 1e3) {
32
+ return `${Math.round(bitrate / 1e3)} Kbps`;
33
+ }
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
+ var HLS_ERROR_TYPES = {
47
+ NETWORK_ERROR: "networkError",
48
+ MEDIA_ERROR: "mediaError",
49
+ MUX_ERROR: "muxError"
50
+ };
51
+ function mapErrorType(hlsType) {
52
+ switch (hlsType) {
53
+ case HLS_ERROR_TYPES.NETWORK_ERROR:
54
+ return "network";
55
+ case HLS_ERROR_TYPES.MEDIA_ERROR:
56
+ return "media";
57
+ case HLS_ERROR_TYPES.MUX_ERROR:
58
+ return "mux";
59
+ default:
60
+ return "other";
61
+ }
62
+ }
63
+ function parseHlsError(data) {
64
+ return {
65
+ type: mapErrorType(data.type),
66
+ details: data.details || "Unknown error",
67
+ fatal: data.fatal || false,
68
+ url: data.url,
69
+ reason: data.reason,
70
+ response: data.response
71
+ };
72
+ }
73
+ function setupHlsEventHandlers(hls, api, callbacks) {
74
+ const handlers = [];
75
+ const addHandler = (event, handler) => {
76
+ hls.on(event, handler);
77
+ handlers.push({ event, handler });
78
+ };
79
+ addHandler("hlsManifestParsed", (_event, data) => {
80
+ api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
81
+ const levels = data.levels.map((level, index) => ({
82
+ id: `level-${index}`,
83
+ label: formatLevel(level),
84
+ width: level.width,
85
+ height: level.height,
86
+ bitrate: level.bitrate,
87
+ active: index === hls.currentLevel
88
+ }));
89
+ api.setState("qualities", levels);
90
+ api.emit("quality:levels", {
91
+ levels: levels.map((l) => ({ id: l.id, label: l.label }))
92
+ });
93
+ callbacks.onManifestParsed?.(data.levels);
94
+ });
95
+ addHandler("hlsLevelSwitched", (_event, data) => {
96
+ const level = hls.levels[data.level];
97
+ const isAuto = callbacks.getIsAutoQuality?.() ?? hls.autoLevelEnabled;
98
+ api.logger.debug("HLS level switched", { level: data.level, height: level?.height, auto: isAuto });
99
+ if (level) {
100
+ const label = isAuto ? `Auto (${formatLevel(level)})` : formatLevel(level);
101
+ api.setState("currentQuality", {
102
+ id: isAuto ? "auto" : `level-${data.level}`,
103
+ label,
104
+ width: level.width,
105
+ height: level.height,
106
+ bitrate: level.bitrate,
107
+ active: true
108
+ });
109
+ }
110
+ api.emit("quality:change", {
111
+ quality: level ? formatLevel(level) : "auto",
112
+ auto: isAuto
113
+ });
114
+ callbacks.onLevelSwitched?.(data.level);
115
+ });
116
+ addHandler("hlsFragBuffered", () => {
117
+ api.setState("buffering", false);
118
+ callbacks.onBufferUpdate?.();
119
+ });
120
+ addHandler("hlsFragLoading", () => {
121
+ api.setState("buffering", true);
122
+ });
123
+ addHandler("hlsLevelLoaded", (_event, data) => {
124
+ if (data.details?.live !== void 0) {
125
+ api.setState("live", data.details.live);
126
+ callbacks.onLiveUpdate?.();
127
+ }
128
+ });
129
+ addHandler("hlsError", (_event, data) => {
130
+ const error = parseHlsError(data);
131
+ api.logger.warn("HLS error", { error });
132
+ callbacks.onError?.(error);
133
+ });
134
+ return () => {
135
+ for (const { event, handler } of handlers) {
136
+ hls.off(event, handler);
137
+ }
138
+ handlers.length = 0;
139
+ };
140
+ }
141
+ function setupVideoEventHandlers(video, api) {
142
+ const handlers = [];
143
+ const addHandler = (event, handler) => {
144
+ video.addEventListener(event, handler);
145
+ handlers.push({ event, handler });
146
+ };
147
+ addHandler("playing", () => {
148
+ api.setState("playing", true);
149
+ api.setState("paused", false);
150
+ api.setState("playbackState", "playing");
151
+ });
152
+ addHandler("pause", () => {
153
+ api.setState("playing", false);
154
+ api.setState("paused", true);
155
+ api.setState("playbackState", "paused");
156
+ });
157
+ addHandler("ended", () => {
158
+ api.setState("playing", false);
159
+ api.setState("ended", true);
160
+ api.setState("playbackState", "ended");
161
+ api.emit("playback:ended", void 0);
162
+ });
163
+ addHandler("timeupdate", () => {
164
+ api.setState("currentTime", video.currentTime);
165
+ api.emit("playback:timeupdate", { currentTime: video.currentTime });
166
+ });
167
+ addHandler("durationchange", () => {
168
+ api.setState("duration", video.duration || 0);
169
+ api.emit("media:loadedmetadata", { duration: video.duration || 0 });
170
+ });
171
+ addHandler("waiting", () => {
172
+ api.setState("waiting", true);
173
+ api.setState("buffering", true);
174
+ api.emit("media:waiting", void 0);
175
+ });
176
+ addHandler("canplay", () => {
177
+ api.setState("waiting", false);
178
+ api.setState("playbackState", "ready");
179
+ api.emit("media:canplay", void 0);
180
+ });
181
+ addHandler("canplaythrough", () => {
182
+ api.setState("buffering", false);
183
+ api.emit("media:canplaythrough", void 0);
184
+ });
185
+ addHandler("progress", () => {
186
+ if (video.buffered.length > 0) {
187
+ const bufferedEnd = video.buffered.end(video.buffered.length - 1);
188
+ const bufferedAmount = video.duration > 0 ? bufferedEnd / video.duration : 0;
189
+ api.setState("bufferedAmount", bufferedAmount);
190
+ api.setState("buffered", video.buffered);
191
+ api.emit("media:progress", { buffered: bufferedAmount });
192
+ }
193
+ });
194
+ addHandler("seeking", () => {
195
+ api.setState("seeking", true);
196
+ });
197
+ addHandler("seeked", () => {
198
+ api.setState("seeking", false);
199
+ api.emit("playback:seeked", { time: video.currentTime });
200
+ });
201
+ addHandler("volumechange", () => {
202
+ api.setState("volume", video.volume);
203
+ api.setState("muted", video.muted);
204
+ api.emit("volume:change", { volume: video.volume, muted: video.muted });
205
+ });
206
+ addHandler("ratechange", () => {
207
+ api.setState("playbackRate", video.playbackRate);
208
+ api.emit("playback:ratechange", { rate: video.playbackRate });
209
+ });
210
+ addHandler("loadedmetadata", () => {
211
+ api.setState("duration", video.duration);
212
+ api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
213
+ });
214
+ addHandler("error", () => {
215
+ const error = video.error;
216
+ if (error) {
217
+ api.logger.error("Video element error", { code: error.code, message: error.message });
218
+ api.emit("media:error", { error: new Error(error.message || "Video playback error") });
219
+ }
220
+ });
221
+ addHandler("enterpictureinpicture", () => {
222
+ api.setState("pip", true);
223
+ api.logger.debug("PiP: entered (standard)");
224
+ });
225
+ addHandler("leavepictureinpicture", () => {
226
+ api.setState("pip", false);
227
+ api.logger.debug("PiP: exited (standard)");
228
+ if (!video.paused || api.getState("playing")) {
229
+ video.play().catch(() => {
230
+ });
231
+ }
232
+ });
233
+ const webkitVideo = video;
234
+ if ("webkitPresentationMode" in video) {
235
+ addHandler("webkitpresentationmodechanged", () => {
236
+ const mode = webkitVideo.webkitPresentationMode;
237
+ const isInPip = mode === "picture-in-picture";
238
+ api.setState("pip", isInPip);
239
+ api.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
240
+ if (mode === "inline" && video.paused) {
241
+ video.play().catch(() => {
242
+ });
243
+ }
244
+ });
245
+ }
246
+ return () => {
247
+ for (const { event, handler } of handlers) {
248
+ video.removeEventListener(event, handler);
249
+ }
250
+ handlers.length = 0;
251
+ };
252
+ }
253
+ var hlsConstructor = null;
254
+ var loadingPromise = null;
255
+ function supportsNativeHLS() {
256
+ if (typeof document === "undefined") return false;
257
+ const video = document.createElement("video");
258
+ return video.canPlayType("application/vnd.apple.mpegurl") !== "";
259
+ }
260
+ function isHlsJsSupported() {
261
+ if (hlsConstructor) {
262
+ return hlsConstructor.isSupported();
263
+ }
264
+ if (typeof window === "undefined") return false;
265
+ return !!(window.MediaSource || window.WebKitMediaSource);
266
+ }
267
+ function isHLSSupported() {
268
+ return supportsNativeHLS() || isHlsJsSupported();
269
+ }
270
+ async function loadHlsJs() {
271
+ if (hlsConstructor) {
272
+ return hlsConstructor;
273
+ }
274
+ if (loadingPromise) {
275
+ return loadingPromise;
276
+ }
277
+ loadingPromise = (async () => {
278
+ try {
279
+ const hlsModule = await import("./hls.js");
280
+ hlsConstructor = hlsModule.default;
281
+ if (!hlsConstructor.isSupported()) {
282
+ throw new Error("hls.js is not supported in this browser");
283
+ }
284
+ return hlsConstructor;
285
+ } catch (error) {
286
+ loadingPromise = null;
287
+ throw new Error(
288
+ `Failed to load hls.js: ${error instanceof Error ? error.message : "Unknown error"}`
289
+ );
290
+ }
291
+ })();
292
+ return loadingPromise;
293
+ }
294
+ function createHlsInstance(config) {
295
+ if (!hlsConstructor) {
296
+ throw new Error("hls.js is not loaded. Call loadHlsJs() first.");
297
+ }
298
+ return new hlsConstructor(config);
299
+ }
300
+ function getHlsConstructor() {
301
+ return hlsConstructor;
302
+ }
303
+ var DEFAULT_CONFIG$3 = {
304
+ debug: false,
305
+ autoStartLoad: true,
306
+ startPosition: -1,
307
+ lowLatencyMode: false,
308
+ maxBufferLength: 30,
309
+ maxMaxBufferLength: 600,
310
+ backBufferLength: 30,
311
+ enableWorker: true,
312
+ // Error recovery settings
313
+ maxNetworkRetries: 3,
314
+ maxMediaRetries: 2,
315
+ retryDelayMs: 1e3,
316
+ retryBackoffFactor: 2
317
+ };
318
+ function createHLSPlugin(config) {
319
+ const mergedConfig = { ...DEFAULT_CONFIG$3, ...config };
320
+ let api = null;
321
+ let hls = null;
322
+ let video = null;
323
+ let isNative = false;
324
+ let currentSrc = null;
325
+ let cleanupHlsEvents = null;
326
+ let cleanupVideoEvents = null;
327
+ let isAutoQuality = true;
328
+ let networkRetryCount = 0;
329
+ let mediaRetryCount = 0;
330
+ let retryTimeout = null;
331
+ let errorCount = 0;
332
+ let errorWindowStart = 0;
333
+ const MAX_ERRORS_IN_WINDOW = 10;
334
+ const ERROR_WINDOW_MS = 5e3;
335
+ const getOrCreateVideo = () => {
336
+ if (video) return video;
337
+ const existing = api?.container.querySelector("video");
338
+ if (existing) {
339
+ video = existing;
340
+ return video;
341
+ }
342
+ video = document.createElement("video");
343
+ video.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:block;object-fit:contain;background:#000";
344
+ video.preload = "metadata";
345
+ video.controls = false;
346
+ video.playsInline = true;
347
+ const poster = api?.getState("poster");
348
+ if (poster) {
349
+ video.poster = poster;
350
+ }
351
+ api?.container.appendChild(video);
352
+ return video;
353
+ };
354
+ const cleanup = () => {
355
+ cleanupHlsEvents?.();
356
+ cleanupHlsEvents = null;
357
+ cleanupVideoEvents?.();
358
+ cleanupVideoEvents = null;
359
+ if (retryTimeout) {
360
+ clearTimeout(retryTimeout);
361
+ retryTimeout = null;
362
+ }
363
+ if (hls) {
364
+ hls.destroy();
365
+ hls = null;
366
+ }
367
+ currentSrc = null;
368
+ isNative = false;
369
+ isAutoQuality = true;
370
+ networkRetryCount = 0;
371
+ mediaRetryCount = 0;
372
+ errorCount = 0;
373
+ errorWindowStart = 0;
374
+ };
375
+ const buildHlsConfig = () => ({
376
+ debug: mergedConfig.debug,
377
+ autoStartLoad: mergedConfig.autoStartLoad,
378
+ startPosition: mergedConfig.startPosition,
379
+ startLevel: -1,
380
+ // Auto quality selection (ABR)
381
+ lowLatencyMode: mergedConfig.lowLatencyMode,
382
+ maxBufferLength: mergedConfig.maxBufferLength,
383
+ maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
384
+ backBufferLength: mergedConfig.backBufferLength,
385
+ enableWorker: mergedConfig.enableWorker,
386
+ // Minimize hls.js internal retries - we handle retries ourselves
387
+ fragLoadingMaxRetry: 1,
388
+ manifestLoadingMaxRetry: 1,
389
+ levelLoadingMaxRetry: 1,
390
+ fragLoadingRetryDelay: 500,
391
+ manifestLoadingRetryDelay: 500,
392
+ levelLoadingRetryDelay: 500
393
+ });
394
+ const getRetryDelay = (retryCount) => {
395
+ const baseDelay = mergedConfig.retryDelayMs ?? 1e3;
396
+ const backoffFactor = mergedConfig.retryBackoffFactor ?? 2;
397
+ return baseDelay * Math.pow(backoffFactor, retryCount);
398
+ };
399
+ const emitFatalError = (error, retriesExhausted) => {
400
+ const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
401
+ api?.logger.error(message, { type: error.type, details: error.details });
402
+ api?.setState("playbackState", "error");
403
+ api?.setState("buffering", false);
404
+ api?.emit("error", {
405
+ code: "MEDIA_ERROR",
406
+ message,
407
+ fatal: true,
408
+ timestamp: Date.now()
409
+ });
410
+ };
411
+ const handleHlsError = (error) => {
412
+ const Hls = getHlsConstructor();
413
+ if (!Hls || !hls) return;
414
+ const now = Date.now();
415
+ if (now - errorWindowStart > ERROR_WINDOW_MS) {
416
+ errorCount = 1;
417
+ errorWindowStart = now;
418
+ } else {
419
+ errorCount++;
420
+ }
421
+ if (errorCount >= MAX_ERRORS_IN_WINDOW) {
422
+ api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
423
+ emitFatalError(error, true);
424
+ cleanupHlsEvents?.();
425
+ cleanupHlsEvents = null;
426
+ hls.destroy();
427
+ hls = null;
428
+ return;
429
+ }
430
+ if (error.fatal) {
431
+ api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
432
+ switch (error.type) {
433
+ case "network": {
434
+ const maxRetries = mergedConfig.maxNetworkRetries ?? 3;
435
+ if (networkRetryCount >= maxRetries) {
436
+ api?.logger.error(`Network error recovery failed after ${networkRetryCount} attempts`);
437
+ emitFatalError(error, true);
438
+ return;
439
+ }
440
+ networkRetryCount++;
441
+ const delay = getRetryDelay(networkRetryCount - 1);
442
+ api?.logger.info(`Attempting network error recovery (attempt ${networkRetryCount}/${maxRetries}) in ${delay}ms`);
443
+ api?.emit("error:network", { error: new Error(error.details) });
444
+ if (retryTimeout) {
445
+ clearTimeout(retryTimeout);
446
+ }
447
+ retryTimeout = setTimeout(() => {
448
+ if (hls) {
449
+ hls.startLoad();
450
+ }
451
+ }, delay);
452
+ break;
453
+ }
454
+ case "media": {
455
+ const maxRetries = mergedConfig.maxMediaRetries ?? 2;
456
+ if (mediaRetryCount >= maxRetries) {
457
+ api?.logger.error(`Media error recovery failed after ${mediaRetryCount} attempts`);
458
+ emitFatalError(error, true);
459
+ return;
460
+ }
461
+ mediaRetryCount++;
462
+ const delay = getRetryDelay(mediaRetryCount - 1);
463
+ api?.logger.info(`Attempting media error recovery (attempt ${mediaRetryCount}/${maxRetries}) in ${delay}ms`);
464
+ api?.emit("error:media", { error: new Error(error.details) });
465
+ if (retryTimeout) {
466
+ clearTimeout(retryTimeout);
467
+ }
468
+ retryTimeout = setTimeout(() => {
469
+ if (hls) {
470
+ hls.recoverMediaError();
471
+ }
472
+ }, delay);
473
+ break;
474
+ }
475
+ default:
476
+ emitFatalError(error, false);
477
+ break;
478
+ }
479
+ }
480
+ };
481
+ const loadNative = async (src) => {
482
+ const videoEl = getOrCreateVideo();
483
+ isNative = true;
484
+ if (api) {
485
+ cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
486
+ }
487
+ return new Promise((resolve, reject) => {
488
+ const onLoaded = () => {
489
+ videoEl.removeEventListener("loadedmetadata", onLoaded);
490
+ videoEl.removeEventListener("error", onError);
491
+ api?.setState("source", { src, type: "application/x-mpegURL" });
492
+ api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
493
+ resolve();
494
+ };
495
+ const onError = () => {
496
+ videoEl.removeEventListener("loadedmetadata", onLoaded);
497
+ videoEl.removeEventListener("error", onError);
498
+ const error = videoEl.error;
499
+ reject(new Error(error?.message || "Failed to load HLS source"));
500
+ };
501
+ videoEl.addEventListener("loadedmetadata", onLoaded);
502
+ videoEl.addEventListener("error", onError);
503
+ videoEl.src = src;
504
+ videoEl.load();
505
+ });
506
+ };
507
+ const loadWithHlsJs = async (src) => {
508
+ await loadHlsJs();
509
+ const videoEl = getOrCreateVideo();
510
+ isNative = false;
511
+ hls = createHlsInstance(buildHlsConfig());
512
+ if (api) {
513
+ cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
514
+ }
515
+ return new Promise((resolve, reject) => {
516
+ if (!hls || !api) {
517
+ reject(new Error("HLS not initialized"));
518
+ return;
519
+ }
520
+ let resolved = false;
521
+ cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
522
+ onManifestParsed: () => {
523
+ if (!resolved) {
524
+ resolved = true;
525
+ api?.setState("source", { src, type: "application/x-mpegURL" });
526
+ api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
527
+ resolve();
528
+ }
529
+ },
530
+ onLevelSwitched: () => {
531
+ },
532
+ onError: (error) => {
533
+ handleHlsError(error);
534
+ if (error.fatal && !resolved && error.type !== "network" && error.type !== "media") {
535
+ resolved = true;
536
+ reject(new Error(error.details));
537
+ }
538
+ },
539
+ getIsAutoQuality: () => isAutoQuality
540
+ });
541
+ hls.attachMedia(videoEl);
542
+ hls.loadSource(src);
543
+ });
544
+ };
545
+ const plugin = {
546
+ id: "hls-provider",
547
+ name: "HLS Provider",
548
+ version: "1.0.0",
549
+ type: "provider",
550
+ description: "HLS playback provider using hls.js",
551
+ canPlay(src) {
552
+ if (!isHLSSupported()) return false;
553
+ const url = src.toLowerCase();
554
+ const urlWithoutQuery = url.split("?")[0].split("#")[0];
555
+ if (urlWithoutQuery.endsWith(".m3u8")) return true;
556
+ if (url.includes("application/x-mpegurl")) return true;
557
+ if (url.includes("application/vnd.apple.mpegurl")) return true;
558
+ return false;
559
+ },
560
+ async init(pluginApi) {
561
+ api = pluginApi;
562
+ api.logger.info("HLS plugin initialized");
563
+ const unsubPlay = api.on("playback:play", async () => {
564
+ if (!video) return;
565
+ try {
566
+ await video.play();
567
+ } catch (e) {
568
+ api?.logger.error("Play failed", e);
569
+ }
570
+ });
571
+ const unsubPause = api.on("playback:pause", () => {
572
+ video?.pause();
573
+ });
574
+ const unsubSeek = api.on("playback:seeking", ({ time }) => {
575
+ if (!video) return;
576
+ const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
577
+ video.currentTime = clampedTime;
578
+ });
579
+ const unsubVolume = api.on("volume:change", ({ volume }) => {
580
+ if (video) video.volume = volume;
581
+ });
582
+ const unsubMute = api.on("volume:mute", ({ muted }) => {
583
+ if (video) video.muted = muted;
584
+ });
585
+ const unsubRate = api.on("playback:ratechange", ({ rate }) => {
586
+ if (video) video.playbackRate = rate;
587
+ });
588
+ const unsubQuality = api.on("quality:select", ({ quality, auto }) => {
589
+ if (!hls || isNative) {
590
+ api?.logger.warn("Quality selection not available");
591
+ return;
592
+ }
593
+ if (auto || quality === "auto") {
594
+ isAutoQuality = true;
595
+ hls.currentLevel = -1;
596
+ api?.logger.debug("Quality: auto selection enabled");
597
+ api?.setState("currentQuality", {
598
+ id: "auto",
599
+ label: "Auto",
600
+ width: 0,
601
+ height: 0,
602
+ bitrate: 0,
603
+ active: true
604
+ });
605
+ } else {
606
+ isAutoQuality = false;
607
+ const levelIndex = parseInt(quality.replace("level-", ""), 10);
608
+ if (!isNaN(levelIndex) && levelIndex >= 0 && levelIndex < hls.levels.length) {
609
+ hls.nextLevel = levelIndex;
610
+ api?.logger.debug(`Quality: queued switch to level ${levelIndex}`);
611
+ }
612
+ }
613
+ });
614
+ api.onDestroy(() => {
615
+ unsubPlay();
616
+ unsubPause();
617
+ unsubSeek();
618
+ unsubVolume();
619
+ unsubMute();
620
+ unsubRate();
621
+ unsubQuality();
622
+ });
623
+ },
624
+ async destroy() {
625
+ api?.logger.info("HLS plugin destroying");
626
+ cleanup();
627
+ if (video?.parentNode) {
628
+ video.parentNode.removeChild(video);
629
+ }
630
+ video = null;
631
+ api = null;
632
+ },
633
+ async loadSource(src) {
634
+ if (!api) throw new Error("Plugin not initialized");
635
+ api.logger.info("Loading HLS source", { src });
636
+ cleanup();
637
+ currentSrc = src;
638
+ api.setState("playbackState", "loading");
639
+ api.setState("buffering", true);
640
+ if (isHlsJsSupported()) {
641
+ api.logger.info("Using hls.js for HLS playback");
642
+ await loadWithHlsJs(src);
643
+ } else if (supportsNativeHLS()) {
644
+ api.logger.info("Using native HLS playback (hls.js not supported)");
645
+ await loadNative(src);
646
+ } else {
647
+ throw new Error("HLS playback not supported in this browser");
648
+ }
649
+ if (video) {
650
+ const muted = api.getState("muted");
651
+ const volume = api.getState("volume");
652
+ if (muted !== void 0) video.muted = muted;
653
+ if (volume !== void 0) video.volume = volume;
654
+ }
655
+ api.setState("playbackState", "ready");
656
+ api.setState("buffering", false);
657
+ },
658
+ getCurrentLevel() {
659
+ if (isNative || !hls) return -1;
660
+ return hls.currentLevel;
661
+ },
662
+ setLevel(index) {
663
+ if (isNative || !hls) {
664
+ api?.logger.warn("Quality selection not available in native HLS mode");
665
+ return;
666
+ }
667
+ hls.currentLevel = index;
668
+ },
669
+ getLevels() {
670
+ if (isNative || !hls) return [];
671
+ return mapLevels(hls.levels, hls.currentLevel);
672
+ },
673
+ getHlsInstance() {
674
+ return hls;
675
+ },
676
+ isNativeHLS() {
677
+ return isNative;
678
+ },
679
+ getLiveInfo() {
680
+ if (isNative || !hls) return null;
681
+ const live = api?.getState("live") || false;
682
+ if (!live) return null;
683
+ return {
684
+ isLive: true,
685
+ latency: hls.latency || 0,
686
+ targetLatency: hls.targetLatency || 3,
687
+ drift: hls.drift || 0
688
+ };
689
+ },
690
+ /**
691
+ * Switch from hls.js to native HLS playback.
692
+ * Used for AirPlay compatibility in Safari.
693
+ * Preserves current playback position.
694
+ */
695
+ async switchToNative() {
696
+ if (isNative) {
697
+ api?.logger.debug("Already using native HLS");
698
+ return;
699
+ }
700
+ if (!supportsNativeHLS()) {
701
+ api?.logger.warn("Native HLS not supported in this browser");
702
+ return;
703
+ }
704
+ if (!currentSrc) {
705
+ api?.logger.warn("No source loaded");
706
+ return;
707
+ }
708
+ api?.logger.info("Switching to native HLS for AirPlay");
709
+ const wasPlaying = api?.getState("playing") || false;
710
+ const currentTime = video?.currentTime || 0;
711
+ const savedSrc = currentSrc;
712
+ cleanup();
713
+ await loadNative(savedSrc);
714
+ if (video && currentTime > 0) {
715
+ video.currentTime = currentTime;
716
+ }
717
+ if (wasPlaying && video) {
718
+ try {
719
+ await video.play();
720
+ } catch (e) {
721
+ api?.logger.debug("Could not auto-resume after switch");
722
+ }
723
+ }
724
+ api?.logger.info("Switched to native HLS");
725
+ },
726
+ /**
727
+ * Switch from native HLS back to hls.js.
728
+ * Restores quality control after AirPlay session ends.
729
+ */
730
+ async switchToHlsJs() {
731
+ if (!isNative) {
732
+ api?.logger.debug("Already using hls.js");
733
+ return;
734
+ }
735
+ if (!isHlsJsSupported()) {
736
+ api?.logger.warn("hls.js not supported in this browser");
737
+ return;
738
+ }
739
+ if (!currentSrc) {
740
+ api?.logger.warn("No source loaded");
741
+ return;
742
+ }
743
+ api?.logger.info("Switching back to hls.js");
744
+ const wasPlaying = api?.getState("playing") || false;
745
+ const currentTime = video?.currentTime || 0;
746
+ const savedSrc = currentSrc;
747
+ cleanup();
748
+ await loadWithHlsJs(savedSrc);
749
+ if (video && currentTime > 0) {
750
+ video.currentTime = currentTime;
751
+ }
752
+ if (wasPlaying && video) {
753
+ try {
754
+ await video.play();
755
+ } catch (e) {
756
+ api?.logger.debug("Could not auto-resume after switch");
757
+ }
758
+ }
759
+ api?.logger.info("Switched to hls.js");
760
+ }
761
+ };
762
+ return plugin;
763
+ }
764
+ var DEFAULT_THEME = {
765
+ primary: "#6366f1",
766
+ background: "#18181b",
767
+ text: "#fafafa",
768
+ textSecondary: "#a1a1aa",
769
+ progressBackground: "#3f3f46",
770
+ progressFill: "#6366f1",
771
+ borderRadius: "12px",
772
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
773
+ };
774
+ var DEFAULT_CONFIG$2 = {
775
+ layout: "full",
776
+ showArtwork: true,
777
+ showTitle: true,
778
+ showArtist: true,
779
+ showTime: true,
780
+ showVolume: true,
781
+ showShuffle: true,
782
+ showRepeat: true,
783
+ showNavigation: true,
784
+ classPrefix: "scarlett-audio",
785
+ autoHide: 0,
786
+ theme: DEFAULT_THEME
787
+ };
788
+ function formatTime(seconds) {
789
+ if (!isFinite(seconds) || seconds < 0) return "0:00";
790
+ const h = Math.floor(seconds / 3600);
791
+ const m = Math.floor(seconds % 3600 / 60);
792
+ const s = Math.floor(seconds % 60);
793
+ if (h > 0) {
794
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
795
+ }
796
+ return `${m}:${s.toString().padStart(2, "0")}`;
797
+ }
798
+ function createStyles(prefix, theme) {
799
+ return `
800
+ .${prefix} {
801
+ font-family: ${theme.fontFamily};
802
+ background: ${theme.background};
803
+ color: ${theme.text};
804
+ border-radius: ${theme.borderRadius};
805
+ overflow: hidden;
806
+ user-select: none;
807
+ }
808
+
809
+ .${prefix}--full {
810
+ display: flex;
811
+ flex-direction: column;
812
+ padding: 20px;
813
+ gap: 16px;
814
+ max-width: 400px;
815
+ }
816
+
817
+ .${prefix}--compact {
818
+ display: flex;
819
+ align-items: center;
820
+ padding: 12px 16px;
821
+ gap: 12px;
822
+ }
823
+
824
+ .${prefix}--mini {
825
+ display: flex;
826
+ align-items: center;
827
+ padding: 8px 12px;
828
+ gap: 8px;
829
+ }
830
+
831
+ .${prefix}__artwork {
832
+ flex-shrink: 0;
833
+ background: ${theme.progressBackground};
834
+ border-radius: 8px;
835
+ overflow: hidden;
836
+ display: flex;
837
+ align-items: center;
838
+ justify-content: center;
839
+ }
840
+
841
+ .${prefix}--full .${prefix}__artwork {
842
+ width: 100%;
843
+ aspect-ratio: 1;
844
+ border-radius: ${theme.borderRadius};
845
+ }
846
+
847
+ .${prefix}--compact .${prefix}__artwork {
848
+ width: 56px;
849
+ height: 56px;
850
+ }
851
+
852
+ .${prefix}--mini .${prefix}__artwork {
853
+ width: 40px;
854
+ height: 40px;
855
+ border-radius: 6px;
856
+ }
857
+
858
+ .${prefix}__artwork img {
859
+ width: 100%;
860
+ height: 100%;
861
+ object-fit: cover;
862
+ }
863
+
864
+ .${prefix}__artwork-placeholder {
865
+ width: 50%;
866
+ height: 50%;
867
+ opacity: 0.3;
868
+ }
869
+
870
+ .${prefix}__info {
871
+ flex: 1;
872
+ min-width: 0;
873
+ display: flex;
874
+ flex-direction: column;
875
+ gap: 4px;
876
+ }
877
+
878
+ .${prefix}__title {
879
+ font-size: 16px;
880
+ font-weight: 600;
881
+ white-space: nowrap;
882
+ overflow: hidden;
883
+ text-overflow: ellipsis;
884
+ }
885
+
886
+ .${prefix}--mini .${prefix}__title {
887
+ font-size: 14px;
888
+ display: inline-block;
889
+ animation: none;
890
+ }
891
+
892
+ .${prefix}__title-wrapper {
893
+ overflow: hidden;
894
+ width: 100%;
895
+ }
896
+
897
+ .${prefix}--mini .${prefix}__title-wrapper .${prefix}__title.scrolling {
898
+ animation: marquee 8s linear infinite;
899
+ }
900
+
901
+ @keyframes marquee {
902
+ 0% { transform: translateX(0); }
903
+ 100% { transform: translateX(-50%); }
904
+ }
905
+
906
+ .${prefix}--mini .${prefix}__progress {
907
+ margin-top: 4px;
908
+ }
909
+
910
+ .${prefix}--mini .${prefix}__progress-bar {
911
+ height: 4px;
912
+ }
913
+
914
+ .${prefix}__artist {
915
+ font-size: 14px;
916
+ color: ${theme.textSecondary};
917
+ white-space: nowrap;
918
+ overflow: hidden;
919
+ text-overflow: ellipsis;
920
+ }
921
+
922
+ .${prefix}--mini .${prefix}__artist {
923
+ font-size: 12px;
924
+ }
925
+
926
+ .${prefix}__progress {
927
+ display: flex;
928
+ align-items: center;
929
+ gap: 12px;
930
+ }
931
+
932
+ .${prefix}__progress-bar {
933
+ flex: 1;
934
+ height: 6px;
935
+ background: ${theme.progressBackground};
936
+ border-radius: 3px;
937
+ cursor: pointer;
938
+ position: relative;
939
+ overflow: hidden;
940
+ }
941
+
942
+ .${prefix}__progress-bar:hover {
943
+ height: 8px;
944
+ }
945
+
946
+ .${prefix}__progress-fill {
947
+ height: 100%;
948
+ background: ${theme.progressFill};
949
+ border-radius: 3px;
950
+ width: 100%;
951
+ transform-origin: left center;
952
+ will-change: transform;
953
+ }
954
+
955
+ .${prefix}__progress-buffered {
956
+ position: absolute;
957
+ top: 0;
958
+ left: 0;
959
+ height: 100%;
960
+ background: ${theme.progressBackground};
961
+ opacity: 0.5;
962
+ border-radius: 3px;
963
+ }
964
+
965
+ .${prefix}__time {
966
+ font-size: 12px;
967
+ color: ${theme.textSecondary};
968
+ min-width: 40px;
969
+ text-align: center;
970
+ }
971
+
972
+ .${prefix}__controls {
973
+ display: flex;
974
+ align-items: center;
975
+ justify-content: center;
976
+ gap: 8px;
977
+ }
978
+
979
+ .${prefix}__btn {
980
+ background: transparent;
981
+ border: none;
982
+ color: ${theme.text};
983
+ cursor: pointer;
984
+ padding: 8px;
985
+ border-radius: 50%;
986
+ display: flex;
987
+ align-items: center;
988
+ justify-content: center;
989
+ transition: background 0.2s, transform 0.1s;
990
+ }
991
+
992
+ .${prefix}__btn:hover {
993
+ background: rgba(255, 255, 255, 0.1);
994
+ }
995
+
996
+ .${prefix}__btn:active {
997
+ transform: scale(0.95);
998
+ }
999
+
1000
+ .${prefix}__btn--primary {
1001
+ background: ${theme.primary};
1002
+ width: 48px;
1003
+ height: 48px;
1004
+ }
1005
+
1006
+ .${prefix}__btn--primary:hover {
1007
+ background: ${theme.primary};
1008
+ opacity: 0.9;
1009
+ }
1010
+
1011
+ .${prefix}--mini .${prefix}__btn--primary {
1012
+ width: 36px;
1013
+ height: 36px;
1014
+ }
1015
+
1016
+ .${prefix}__btn--active {
1017
+ color: ${theme.primary};
1018
+ }
1019
+
1020
+ .${prefix}__btn svg {
1021
+ width: 20px;
1022
+ height: 20px;
1023
+ fill: currentColor;
1024
+ }
1025
+
1026
+ .${prefix}__btn--primary svg {
1027
+ width: 24px;
1028
+ height: 24px;
1029
+ }
1030
+
1031
+ .${prefix}__volume {
1032
+ display: flex;
1033
+ align-items: center;
1034
+ gap: 8px;
1035
+ }
1036
+
1037
+ .${prefix}__volume-slider {
1038
+ width: 80px;
1039
+ height: 4px;
1040
+ background: ${theme.progressBackground};
1041
+ border-radius: 2px;
1042
+ cursor: pointer;
1043
+ position: relative;
1044
+ }
1045
+
1046
+ .${prefix}__volume-fill {
1047
+ height: 100%;
1048
+ background: ${theme.text};
1049
+ border-radius: 2px;
1050
+ }
1051
+
1052
+ .${prefix}__secondary-controls {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ justify-content: space-between;
1056
+ }
1057
+
1058
+ .${prefix}--hidden {
1059
+ display: none;
1060
+ }
1061
+ `;
1062
+ }
1063
+ var ICONS = {
1064
+ play: `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>`,
1065
+ pause: `<svg viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`,
1066
+ previous: `<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>`,
1067
+ next: `<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>`,
1068
+ shuffle: `<svg viewBox="0 0 24 24"><path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/></svg>`,
1069
+ repeatOff: `<svg viewBox="0 0 24 24"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>`,
1070
+ repeatAll: `<svg viewBox="0 0 24 24"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>`,
1071
+ repeatOne: `<svg viewBox="0 0 24 24"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4zm-4-2V9h-1l-2 1v1h1.5v4H13z"/></svg>`,
1072
+ volumeHigh: `<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
1073
+ volumeMuted: `<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`,
1074
+ music: `<svg viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>`
1075
+ };
1076
+ function createAudioUIPlugin(config) {
1077
+ const mergedConfig = { ...DEFAULT_CONFIG$2, ...config };
1078
+ const theme = { ...DEFAULT_THEME, ...mergedConfig.theme };
1079
+ const prefix = mergedConfig.classPrefix;
1080
+ let api = null;
1081
+ let container = null;
1082
+ let styleElement = null;
1083
+ let layout = mergedConfig.layout;
1084
+ let isVisible = true;
1085
+ let animationFrameId = null;
1086
+ let lastKnownTime = 0;
1087
+ let lastUpdateTimestamp = 0;
1088
+ let isPlaying = false;
1089
+ let artworkImg = null;
1090
+ let titleEl = null;
1091
+ let artistEl = null;
1092
+ let progressFill = null;
1093
+ let currentTimeEl = null;
1094
+ let durationEl = null;
1095
+ let playPauseBtn = null;
1096
+ let shuffleBtn = null;
1097
+ let repeatBtn = null;
1098
+ let volumeBtn = null;
1099
+ let volumeFill = null;
1100
+ const startProgressAnimation = () => {
1101
+ if (animationFrameId !== null) return;
1102
+ const animate = (timestamp) => {
1103
+ if (!api || !isPlaying) {
1104
+ animationFrameId = null;
1105
+ return;
1106
+ }
1107
+ const duration = api.getState("duration") || 0;
1108
+ if (duration <= 0) {
1109
+ animationFrameId = requestAnimationFrame(animate);
1110
+ return;
1111
+ }
1112
+ const elapsed = (timestamp - lastUpdateTimestamp) / 1e3;
1113
+ const interpolatedTime = Math.min(lastKnownTime + elapsed, duration);
1114
+ const scale = interpolatedTime / duration;
1115
+ if (progressFill) {
1116
+ progressFill.style.transform = `scaleX(${scale})`;
1117
+ }
1118
+ if (currentTimeEl) {
1119
+ currentTimeEl.textContent = formatTime(interpolatedTime);
1120
+ }
1121
+ animationFrameId = requestAnimationFrame(animate);
1122
+ };
1123
+ lastUpdateTimestamp = performance.now();
1124
+ animationFrameId = requestAnimationFrame(animate);
1125
+ };
1126
+ const stopProgressAnimation = () => {
1127
+ if (animationFrameId !== null) {
1128
+ cancelAnimationFrame(animationFrameId);
1129
+ animationFrameId = null;
1130
+ }
1131
+ };
1132
+ const createUI = () => {
1133
+ if (!api) return;
1134
+ styleElement = document.createElement("style");
1135
+ styleElement.textContent = createStyles(prefix, theme);
1136
+ document.head.appendChild(styleElement);
1137
+ container = document.createElement("div");
1138
+ container.className = `${prefix} ${prefix}--${layout}`;
1139
+ if (layout === "full") {
1140
+ container.innerHTML = buildFullLayout();
1141
+ } else if (layout === "compact") {
1142
+ container.innerHTML = buildCompactLayout();
1143
+ } else {
1144
+ container.innerHTML = buildMiniLayout();
1145
+ }
1146
+ artworkImg = container.querySelector(`.${prefix}__artwork img`);
1147
+ titleEl = container.querySelector(`.${prefix}__title`);
1148
+ artistEl = container.querySelector(`.${prefix}__artist`);
1149
+ progressFill = container.querySelector(`.${prefix}__progress-fill`);
1150
+ currentTimeEl = container.querySelector(`.${prefix}__time--current`);
1151
+ durationEl = container.querySelector(`.${prefix}__time--duration`);
1152
+ playPauseBtn = container.querySelector(`.${prefix}__btn--play`);
1153
+ shuffleBtn = container.querySelector(`.${prefix}__btn--shuffle`);
1154
+ repeatBtn = container.querySelector(`.${prefix}__btn--repeat`);
1155
+ volumeBtn = container.querySelector(`.${prefix}__btn--volume`);
1156
+ volumeFill = container.querySelector(`.${prefix}__volume-fill`);
1157
+ attachEventListeners();
1158
+ api.container.appendChild(container);
1159
+ };
1160
+ const buildFullLayout = () => {
1161
+ return `
1162
+ ${mergedConfig.showArtwork ? `
1163
+ <div class="${prefix}__artwork">
1164
+ <img src="${mergedConfig.defaultArtwork || ""}" alt="Album art" />
1165
+ ${!mergedConfig.defaultArtwork ? `<div class="${prefix}__artwork-placeholder">${ICONS.music}</div>` : ""}
1166
+ </div>
1167
+ ` : ""}
1168
+ <div class="${prefix}__info">
1169
+ ${mergedConfig.showTitle ? `<div class="${prefix}__title">-</div>` : ""}
1170
+ ${mergedConfig.showArtist ? `<div class="${prefix}__artist">-</div>` : ""}
1171
+ </div>
1172
+ <div class="${prefix}__progress">
1173
+ ${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--current">0:00</span>` : ""}
1174
+ <div class="${prefix}__progress-bar">
1175
+ <div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
1176
+ </div>
1177
+ ${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--duration">0:00</span>` : ""}
1178
+ </div>
1179
+ <div class="${prefix}__controls">
1180
+ ${mergedConfig.showShuffle ? `<button class="${prefix}__btn ${prefix}__btn--shuffle" title="Shuffle">${ICONS.shuffle}</button>` : ""}
1181
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous">${ICONS.previous}</button>` : ""}
1182
+ <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
1183
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next">${ICONS.next}</button>` : ""}
1184
+ ${mergedConfig.showRepeat ? `<button class="${prefix}__btn ${prefix}__btn--repeat" title="Repeat">${ICONS.repeatOff}</button>` : ""}
1185
+ </div>
1186
+ ${mergedConfig.showVolume ? `
1187
+ <div class="${prefix}__secondary-controls">
1188
+ <div class="${prefix}__volume">
1189
+ <button class="${prefix}__btn ${prefix}__btn--volume" title="Volume">${ICONS.volumeHigh}</button>
1190
+ <div class="${prefix}__volume-slider">
1191
+ <div class="${prefix}__volume-fill" style="width: 100%"></div>
1192
+ </div>
1193
+ </div>
1194
+ </div>
1195
+ ` : ""}
1196
+ `;
1197
+ };
1198
+ const buildCompactLayout = () => {
1199
+ return `
1200
+ ${mergedConfig.showArtwork ? `
1201
+ <div class="${prefix}__artwork">
1202
+ <img src="${mergedConfig.defaultArtwork || ""}" alt="Album art" />
1203
+ </div>
1204
+ ` : ""}
1205
+ <div class="${prefix}__info">
1206
+ ${mergedConfig.showTitle ? `<div class="${prefix}__title">-</div>` : ""}
1207
+ ${mergedConfig.showArtist ? `<div class="${prefix}__artist">-</div>` : ""}
1208
+ <div class="${prefix}__progress">
1209
+ <div class="${prefix}__progress-bar">
1210
+ <div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
1211
+ </div>
1212
+ </div>
1213
+ </div>
1214
+ <div class="${prefix}__controls">
1215
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous">${ICONS.previous}</button>` : ""}
1216
+ <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
1217
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next">${ICONS.next}</button>` : ""}
1218
+ </div>
1219
+ `;
1220
+ };
1221
+ const buildMiniLayout = () => {
1222
+ return `
1223
+ <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
1224
+ ${mergedConfig.showArtwork ? `
1225
+ <div class="${prefix}__artwork">
1226
+ <img src="${mergedConfig.defaultArtwork || ""}" alt="Album art" />
1227
+ </div>
1228
+ ` : ""}
1229
+ <div class="${prefix}__info">
1230
+ ${mergedConfig.showTitle ? `<div class="${prefix}__title-wrapper"><div class="${prefix}__title">-</div></div>` : ""}
1231
+ <div class="${prefix}__progress">
1232
+ <div class="${prefix}__progress-bar">
1233
+ <div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
1234
+ </div>
1235
+ </div>
1236
+ </div>
1237
+ `;
1238
+ };
1239
+ const attachEventListeners = () => {
1240
+ if (!container || !api) return;
1241
+ playPauseBtn?.addEventListener("click", () => {
1242
+ const playing = api?.getState("playing");
1243
+ if (playing) {
1244
+ api?.emit("playback:pause", void 0);
1245
+ } else {
1246
+ api?.emit("playback:play", void 0);
1247
+ }
1248
+ });
1249
+ container.querySelector(`.${prefix}__btn--prev`)?.addEventListener("click", () => {
1250
+ const playlist = api?.getPlugin("playlist");
1251
+ if (playlist) {
1252
+ playlist.previous();
1253
+ } else {
1254
+ api?.emit("playback:seeking", { time: 0 });
1255
+ }
1256
+ });
1257
+ container.querySelector(`.${prefix}__btn--next`)?.addEventListener("click", () => {
1258
+ const playlist = api?.getPlugin("playlist");
1259
+ playlist?.next();
1260
+ });
1261
+ shuffleBtn?.addEventListener("click", () => {
1262
+ const playlist = api?.getPlugin("playlist");
1263
+ playlist?.toggleShuffle();
1264
+ });
1265
+ repeatBtn?.addEventListener("click", () => {
1266
+ const playlist = api?.getPlugin("playlist");
1267
+ playlist?.cycleRepeat();
1268
+ });
1269
+ volumeBtn?.addEventListener("click", () => {
1270
+ const muted = api?.getState("muted");
1271
+ api?.emit("volume:mute", { muted: !muted });
1272
+ });
1273
+ const progressBar = container.querySelector(`.${prefix}__progress-bar`);
1274
+ progressBar?.addEventListener("click", (e) => {
1275
+ const mouseEvent = e;
1276
+ const rect = mouseEvent.currentTarget.getBoundingClientRect();
1277
+ const percent = (mouseEvent.clientX - rect.left) / rect.width;
1278
+ const duration = api?.getState("duration") || 0;
1279
+ const time = percent * duration;
1280
+ api?.emit("playback:seeking", { time });
1281
+ });
1282
+ const volumeSlider = container.querySelector(`.${prefix}__volume-slider`);
1283
+ volumeSlider?.addEventListener("click", (e) => {
1284
+ const mouseEvent = e;
1285
+ const rect = mouseEvent.currentTarget.getBoundingClientRect();
1286
+ const percent = Math.max(0, Math.min(1, (mouseEvent.clientX - rect.left) / rect.width));
1287
+ api?.emit("volume:change", { volume: percent, muted: false });
1288
+ });
1289
+ };
1290
+ const updateUI = () => {
1291
+ if (!api || !container) return;
1292
+ const playing = api.getState("playing");
1293
+ const wasPlaying = isPlaying;
1294
+ isPlaying = playing;
1295
+ if (playPauseBtn) {
1296
+ playPauseBtn.innerHTML = playing ? ICONS.pause : ICONS.play;
1297
+ playPauseBtn.title = playing ? "Pause" : "Play";
1298
+ }
1299
+ const currentTime = api.getState("currentTime") || 0;
1300
+ const duration = api.getState("duration") || 0;
1301
+ lastKnownTime = currentTime;
1302
+ lastUpdateTimestamp = performance.now();
1303
+ if (playing && !wasPlaying) {
1304
+ startProgressAnimation();
1305
+ } else if (!playing && wasPlaying) {
1306
+ stopProgressAnimation();
1307
+ }
1308
+ if (!playing) {
1309
+ const scale = duration > 0 ? currentTime / duration : 0;
1310
+ if (progressFill) {
1311
+ progressFill.style.transform = `scaleX(${scale})`;
1312
+ }
1313
+ if (currentTimeEl) {
1314
+ currentTimeEl.textContent = formatTime(currentTime);
1315
+ }
1316
+ }
1317
+ if (durationEl) {
1318
+ durationEl.textContent = formatTime(duration);
1319
+ }
1320
+ const title = api.getState("title");
1321
+ const poster = api.getState("poster");
1322
+ if (titleEl && title) {
1323
+ titleEl.textContent = title;
1324
+ if (layout === "mini") {
1325
+ const wrapper = titleEl.parentElement;
1326
+ if (wrapper && titleEl.scrollWidth > wrapper.clientWidth) {
1327
+ titleEl.textContent = `${title} • ${title} • `;
1328
+ titleEl.classList.add("scrolling");
1329
+ } else {
1330
+ titleEl.classList.remove("scrolling");
1331
+ }
1332
+ }
1333
+ }
1334
+ if (artworkImg && poster) {
1335
+ artworkImg.src = poster;
1336
+ }
1337
+ const volume = api.getState("volume") || 1;
1338
+ const muted = api.getState("muted");
1339
+ if (volumeFill) {
1340
+ volumeFill.style.width = `${(muted ? 0 : volume) * 100}%`;
1341
+ }
1342
+ if (volumeBtn) {
1343
+ volumeBtn.innerHTML = muted || volume === 0 ? ICONS.volumeMuted : ICONS.volumeHigh;
1344
+ }
1345
+ const playlist = api.getPlugin("playlist");
1346
+ if (playlist) {
1347
+ const state = playlist.getState();
1348
+ if (shuffleBtn) {
1349
+ shuffleBtn.classList.toggle(`${prefix}__btn--active`, state.shuffle);
1350
+ }
1351
+ if (repeatBtn) {
1352
+ repeatBtn.classList.toggle(`${prefix}__btn--active`, state.repeat !== "none");
1353
+ if (state.repeat === "one") {
1354
+ repeatBtn.innerHTML = ICONS.repeatOne;
1355
+ } else if (state.repeat === "all") {
1356
+ repeatBtn.innerHTML = ICONS.repeatAll;
1357
+ } else {
1358
+ repeatBtn.innerHTML = ICONS.repeatOff;
1359
+ }
1360
+ }
1361
+ }
1362
+ };
1363
+ const plugin = {
1364
+ id: "audio-ui",
1365
+ name: "Audio UI",
1366
+ version: "1.0.0",
1367
+ type: "ui",
1368
+ description: "Compact audio player interface",
1369
+ async init(pluginApi) {
1370
+ api = pluginApi;
1371
+ api.logger.info("Audio UI plugin initialized");
1372
+ createUI();
1373
+ const unsubState = api.subscribeToState(() => {
1374
+ updateUI();
1375
+ });
1376
+ const unsubTime = api.on("playback:timeupdate", () => {
1377
+ updateUI();
1378
+ });
1379
+ const unsubPlaylist = api.on("playlist:change", (payload) => {
1380
+ if (payload?.track) {
1381
+ if (titleEl) titleEl.textContent = payload.track.title || "-";
1382
+ if (artistEl) artistEl.textContent = payload.track.artist || "-";
1383
+ if (artworkImg && payload.track.artwork) {
1384
+ artworkImg.src = payload.track.artwork;
1385
+ }
1386
+ }
1387
+ });
1388
+ const unsubShuffle = api.on("playlist:shuffle", () => {
1389
+ updateUI();
1390
+ });
1391
+ const unsubRepeat = api.on("playlist:repeat", () => {
1392
+ updateUI();
1393
+ });
1394
+ api.onDestroy(() => {
1395
+ unsubState();
1396
+ unsubTime();
1397
+ unsubPlaylist();
1398
+ unsubShuffle();
1399
+ unsubRepeat();
1400
+ });
1401
+ updateUI();
1402
+ },
1403
+ async destroy() {
1404
+ api?.logger.info("Audio UI plugin destroying");
1405
+ stopProgressAnimation();
1406
+ if (container?.parentNode) {
1407
+ container.parentNode.removeChild(container);
1408
+ }
1409
+ if (styleElement?.parentNode) {
1410
+ styleElement.parentNode.removeChild(styleElement);
1411
+ }
1412
+ container = null;
1413
+ styleElement = null;
1414
+ api = null;
1415
+ },
1416
+ getElement() {
1417
+ return container;
1418
+ },
1419
+ setLayout(newLayout) {
1420
+ if (!container) return;
1421
+ stopProgressAnimation();
1422
+ layout = newLayout;
1423
+ container.className = `${prefix} ${prefix}--${layout}`;
1424
+ if (layout === "full") {
1425
+ container.innerHTML = buildFullLayout();
1426
+ } else if (layout === "compact") {
1427
+ container.innerHTML = buildCompactLayout();
1428
+ } else {
1429
+ container.innerHTML = buildMiniLayout();
1430
+ }
1431
+ artworkImg = container.querySelector(`.${prefix}__artwork img`);
1432
+ titleEl = container.querySelector(`.${prefix}__title`);
1433
+ artistEl = container.querySelector(`.${prefix}__artist`);
1434
+ progressFill = container.querySelector(`.${prefix}__progress-fill`);
1435
+ currentTimeEl = container.querySelector(`.${prefix}__time--current`);
1436
+ durationEl = container.querySelector(`.${prefix}__time--duration`);
1437
+ playPauseBtn = container.querySelector(`.${prefix}__btn--play`);
1438
+ shuffleBtn = container.querySelector(`.${prefix}__btn--shuffle`);
1439
+ repeatBtn = container.querySelector(`.${prefix}__btn--repeat`);
1440
+ volumeBtn = container.querySelector(`.${prefix}__btn--volume`);
1441
+ volumeFill = container.querySelector(`.${prefix}__volume-fill`);
1442
+ attachEventListeners();
1443
+ updateUI();
1444
+ if (isPlaying) {
1445
+ startProgressAnimation();
1446
+ }
1447
+ },
1448
+ setTheme(newTheme) {
1449
+ Object.assign(theme, newTheme);
1450
+ if (styleElement) {
1451
+ styleElement.textContent = createStyles(prefix, theme);
1452
+ }
1453
+ },
1454
+ show() {
1455
+ isVisible = true;
1456
+ container?.classList.remove(`${prefix}--hidden`);
1457
+ },
1458
+ hide() {
1459
+ isVisible = false;
1460
+ container?.classList.add(`${prefix}--hidden`);
1461
+ },
1462
+ toggle() {
1463
+ if (isVisible) {
1464
+ this.hide();
1465
+ } else {
1466
+ this.show();
1467
+ }
1468
+ }
1469
+ };
1470
+ return plugin;
1471
+ }
1472
+ var DEFAULT_CONFIG$1 = {
1473
+ autoAdvance: true,
1474
+ preloadNext: true,
1475
+ persist: false,
1476
+ persistKey: "scarlett-playlist",
1477
+ shuffle: false,
1478
+ repeat: "none"
1479
+ };
1480
+ function generateId() {
1481
+ return `track-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1482
+ }
1483
+ function shuffleArray(array) {
1484
+ const result = [...array];
1485
+ for (let i = result.length - 1; i > 0; i--) {
1486
+ const j = Math.floor(Math.random() * (i + 1));
1487
+ [result[i], result[j]] = [result[j], result[i]];
1488
+ }
1489
+ return result;
1490
+ }
1491
+ function createPlaylistPlugin(config) {
1492
+ const mergedConfig = { ...DEFAULT_CONFIG$1, ...config };
1493
+ let api = null;
1494
+ let tracks = mergedConfig.tracks || [];
1495
+ let currentIndex = -1;
1496
+ let shuffle = mergedConfig.shuffle || false;
1497
+ let repeat = mergedConfig.repeat || "none";
1498
+ let shuffleOrder = [];
1499
+ tracks = tracks.map((t) => ({ ...t, id: t.id || generateId() }));
1500
+ const generateShuffleOrder = () => {
1501
+ const indices = tracks.map((_, i) => i);
1502
+ shuffleOrder = shuffleArray(indices);
1503
+ if (currentIndex >= 0) {
1504
+ const currentPos = shuffleOrder.indexOf(currentIndex);
1505
+ if (currentPos > 0) {
1506
+ shuffleOrder.splice(currentPos, 1);
1507
+ shuffleOrder.unshift(currentIndex);
1508
+ }
1509
+ }
1510
+ };
1511
+ const getActualIndex = (logicalIndex) => {
1512
+ if (!shuffle || shuffleOrder.length === 0) {
1513
+ return logicalIndex;
1514
+ }
1515
+ return shuffleOrder[logicalIndex] ?? logicalIndex;
1516
+ };
1517
+ const getLogicalIndex = (actualIndex) => {
1518
+ if (!shuffle || shuffleOrder.length === 0) {
1519
+ return actualIndex;
1520
+ }
1521
+ return shuffleOrder.indexOf(actualIndex);
1522
+ };
1523
+ const hasNextTrack = () => {
1524
+ if (tracks.length === 0) return false;
1525
+ if (repeat === "one" || repeat === "all") return true;
1526
+ const logicalIndex = getLogicalIndex(currentIndex);
1527
+ return logicalIndex < tracks.length - 1;
1528
+ };
1529
+ const hasPreviousTrack = () => {
1530
+ if (tracks.length === 0) return false;
1531
+ if (repeat === "one" || repeat === "all") return true;
1532
+ const logicalIndex = getLogicalIndex(currentIndex);
1533
+ return logicalIndex > 0;
1534
+ };
1535
+ const getNextIndex = () => {
1536
+ if (tracks.length === 0) return -1;
1537
+ if (repeat === "one") {
1538
+ return currentIndex;
1539
+ }
1540
+ const logicalIndex = getLogicalIndex(currentIndex);
1541
+ let nextLogical = logicalIndex + 1;
1542
+ if (nextLogical >= tracks.length) {
1543
+ if (repeat === "all") {
1544
+ if (shuffle) {
1545
+ generateShuffleOrder();
1546
+ }
1547
+ nextLogical = 0;
1548
+ } else {
1549
+ return -1;
1550
+ }
1551
+ }
1552
+ return getActualIndex(nextLogical);
1553
+ };
1554
+ const getPreviousIndex = () => {
1555
+ if (tracks.length === 0) return -1;
1556
+ if (repeat === "one") {
1557
+ return currentIndex;
1558
+ }
1559
+ const logicalIndex = getLogicalIndex(currentIndex);
1560
+ let prevLogical = logicalIndex - 1;
1561
+ if (prevLogical < 0) {
1562
+ if (repeat === "all") {
1563
+ prevLogical = tracks.length - 1;
1564
+ } else {
1565
+ return -1;
1566
+ }
1567
+ }
1568
+ return getActualIndex(prevLogical);
1569
+ };
1570
+ const persistPlaylist = () => {
1571
+ if (!mergedConfig.persist) return;
1572
+ try {
1573
+ const data = {
1574
+ tracks,
1575
+ currentIndex,
1576
+ shuffle,
1577
+ repeat,
1578
+ shuffleOrder
1579
+ };
1580
+ localStorage.setItem(mergedConfig.persistKey, JSON.stringify(data));
1581
+ } catch (e) {
1582
+ api?.logger.warn("Failed to persist playlist", e);
1583
+ }
1584
+ };
1585
+ const loadPersistedPlaylist = () => {
1586
+ if (!mergedConfig.persist) return;
1587
+ try {
1588
+ const data = localStorage.getItem(mergedConfig.persistKey);
1589
+ if (data) {
1590
+ const parsed = JSON.parse(data);
1591
+ tracks = parsed.tracks || [];
1592
+ currentIndex = parsed.currentIndex ?? -1;
1593
+ shuffle = parsed.shuffle ?? false;
1594
+ repeat = parsed.repeat ?? "none";
1595
+ shuffleOrder = parsed.shuffleOrder || [];
1596
+ }
1597
+ } catch (e) {
1598
+ api?.logger.warn("Failed to load persisted playlist", e);
1599
+ }
1600
+ };
1601
+ const emitChange = () => {
1602
+ const track = currentIndex >= 0 ? tracks[currentIndex] : null;
1603
+ api?.emit("playlist:change", { track, index: currentIndex });
1604
+ persistPlaylist();
1605
+ };
1606
+ const setCurrentTrack = (index) => {
1607
+ if (index < 0 || index >= tracks.length) {
1608
+ api?.logger.warn("Invalid track index", { index });
1609
+ return;
1610
+ }
1611
+ const track = tracks[index];
1612
+ currentIndex = index;
1613
+ api?.logger.info("Track changed", { index, title: track.title, src: track.src });
1614
+ if (track.title) {
1615
+ api?.setState("title", track.title);
1616
+ }
1617
+ if (track.artwork) {
1618
+ api?.setState("poster", track.artwork);
1619
+ }
1620
+ api?.setState("mediaType", track.type || "audio");
1621
+ emitChange();
1622
+ };
1623
+ const plugin = {
1624
+ id: "playlist",
1625
+ name: "Playlist",
1626
+ version: "1.0.0",
1627
+ type: "feature",
1628
+ description: "Playlist management with shuffle, repeat, and gapless playback",
1629
+ async init(pluginApi) {
1630
+ api = pluginApi;
1631
+ api.logger.info("Playlist plugin initialized");
1632
+ loadPersistedPlaylist();
1633
+ if (shuffle && tracks.length > 0) {
1634
+ generateShuffleOrder();
1635
+ }
1636
+ const unsubEnded = api.on("playback:ended", () => {
1637
+ if (!mergedConfig.autoAdvance) return;
1638
+ const nextIdx = getNextIndex();
1639
+ if (nextIdx >= 0) {
1640
+ api?.logger.debug("Auto-advancing to next track", { nextIdx });
1641
+ setCurrentTrack(nextIdx);
1642
+ } else {
1643
+ api?.logger.info("Playlist ended");
1644
+ api?.emit("playlist:ended", void 0);
1645
+ }
1646
+ });
1647
+ api.onDestroy(() => {
1648
+ unsubEnded();
1649
+ persistPlaylist();
1650
+ });
1651
+ },
1652
+ async destroy() {
1653
+ api?.logger.info("Playlist plugin destroying");
1654
+ persistPlaylist();
1655
+ api = null;
1656
+ },
1657
+ add(trackOrTracks) {
1658
+ const newTracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
1659
+ newTracks.forEach((track) => {
1660
+ const normalizedTrack = { ...track, id: track.id || generateId() };
1661
+ const index = tracks.length;
1662
+ tracks.push(normalizedTrack);
1663
+ api?.emit("playlist:add", { track: normalizedTrack, index });
1664
+ api?.logger.debug("Track added", { title: normalizedTrack.title, index });
1665
+ });
1666
+ if (shuffle) {
1667
+ const startIndex = tracks.length - newTracks.length;
1668
+ for (let i = startIndex; i < tracks.length; i++) {
1669
+ const insertPos = Math.floor(Math.random() * (shuffleOrder.length - getLogicalIndex(currentIndex))) + getLogicalIndex(currentIndex) + 1;
1670
+ shuffleOrder.splice(Math.min(insertPos, shuffleOrder.length), 0, i);
1671
+ }
1672
+ }
1673
+ persistPlaylist();
1674
+ },
1675
+ insert(index, track) {
1676
+ const normalizedTrack = { ...track, id: track.id || generateId() };
1677
+ const clampedIndex = Math.max(0, Math.min(index, tracks.length));
1678
+ tracks.splice(clampedIndex, 0, normalizedTrack);
1679
+ if (currentIndex >= clampedIndex) {
1680
+ currentIndex++;
1681
+ }
1682
+ if (shuffle) {
1683
+ shuffleOrder = shuffleOrder.map((i) => i >= clampedIndex ? i + 1 : i);
1684
+ const insertPos = Math.floor(Math.random() * shuffleOrder.length);
1685
+ shuffleOrder.splice(insertPos, 0, clampedIndex);
1686
+ }
1687
+ api?.emit("playlist:add", { track: normalizedTrack, index: clampedIndex });
1688
+ persistPlaylist();
1689
+ },
1690
+ remove(idOrIndex) {
1691
+ let index;
1692
+ if (typeof idOrIndex === "string") {
1693
+ index = tracks.findIndex((t) => t.id === idOrIndex);
1694
+ if (index === -1) {
1695
+ api?.logger.warn("Track not found", { id: idOrIndex });
1696
+ return;
1697
+ }
1698
+ } else {
1699
+ index = idOrIndex;
1700
+ }
1701
+ if (index < 0 || index >= tracks.length) {
1702
+ api?.logger.warn("Invalid track index", { index });
1703
+ return;
1704
+ }
1705
+ const [removedTrack] = tracks.splice(index, 1);
1706
+ if (index < currentIndex) {
1707
+ currentIndex--;
1708
+ } else if (index === currentIndex) {
1709
+ if (currentIndex >= tracks.length) {
1710
+ currentIndex = tracks.length - 1;
1711
+ }
1712
+ emitChange();
1713
+ }
1714
+ if (shuffle) {
1715
+ shuffleOrder = shuffleOrder.filter((i) => i !== index).map((i) => i > index ? i - 1 : i);
1716
+ }
1717
+ api?.emit("playlist:remove", { track: removedTrack, index });
1718
+ persistPlaylist();
1719
+ },
1720
+ clear() {
1721
+ tracks = [];
1722
+ currentIndex = -1;
1723
+ shuffleOrder = [];
1724
+ api?.emit("playlist:clear", void 0);
1725
+ emitChange();
1726
+ },
1727
+ play(idOrIndex) {
1728
+ let index;
1729
+ if (idOrIndex === void 0) {
1730
+ index = currentIndex >= 0 ? currentIndex : shuffle ? getActualIndex(0) : 0;
1731
+ } else if (typeof idOrIndex === "string") {
1732
+ index = tracks.findIndex((t) => t.id === idOrIndex);
1733
+ if (index === -1) {
1734
+ api?.logger.warn("Track not found", { id: idOrIndex });
1735
+ return;
1736
+ }
1737
+ } else {
1738
+ index = idOrIndex;
1739
+ }
1740
+ if (tracks.length === 0) {
1741
+ api?.logger.warn("Playlist is empty");
1742
+ return;
1743
+ }
1744
+ setCurrentTrack(index);
1745
+ },
1746
+ next() {
1747
+ const nextIdx = getNextIndex();
1748
+ if (nextIdx >= 0) {
1749
+ setCurrentTrack(nextIdx);
1750
+ } else {
1751
+ api?.logger.info("No next track");
1752
+ }
1753
+ },
1754
+ previous() {
1755
+ const currentTime = api?.getState("currentTime") || 0;
1756
+ if (currentTime > 3) {
1757
+ api?.emit("playback:seeking", { time: 0 });
1758
+ return;
1759
+ }
1760
+ const prevIdx = getPreviousIndex();
1761
+ if (prevIdx >= 0) {
1762
+ setCurrentTrack(prevIdx);
1763
+ } else {
1764
+ api?.logger.info("No previous track");
1765
+ }
1766
+ },
1767
+ toggleShuffle() {
1768
+ this.setShuffle(!shuffle);
1769
+ },
1770
+ setShuffle(enabled) {
1771
+ shuffle = enabled;
1772
+ if (enabled) {
1773
+ generateShuffleOrder();
1774
+ } else {
1775
+ shuffleOrder = [];
1776
+ }
1777
+ api?.emit("playlist:shuffle", { enabled });
1778
+ api?.logger.info("Shuffle mode", { enabled });
1779
+ persistPlaylist();
1780
+ },
1781
+ cycleRepeat() {
1782
+ const modes = ["none", "all", "one"];
1783
+ const currentIdx = modes.indexOf(repeat);
1784
+ const nextIdx = (currentIdx + 1) % modes.length;
1785
+ this.setRepeat(modes[nextIdx]);
1786
+ },
1787
+ setRepeat(mode) {
1788
+ repeat = mode;
1789
+ api?.emit("playlist:repeat", { mode });
1790
+ api?.logger.info("Repeat mode", { mode });
1791
+ persistPlaylist();
1792
+ },
1793
+ move(fromIndex, toIndex) {
1794
+ if (fromIndex < 0 || fromIndex >= tracks.length) return;
1795
+ if (toIndex < 0 || toIndex >= tracks.length) return;
1796
+ if (fromIndex === toIndex) return;
1797
+ const [track] = tracks.splice(fromIndex, 1);
1798
+ tracks.splice(toIndex, 0, track);
1799
+ if (currentIndex === fromIndex) {
1800
+ currentIndex = toIndex;
1801
+ } else if (fromIndex < currentIndex && toIndex >= currentIndex) {
1802
+ currentIndex--;
1803
+ } else if (fromIndex > currentIndex && toIndex <= currentIndex) {
1804
+ currentIndex++;
1805
+ }
1806
+ if (shuffle) {
1807
+ generateShuffleOrder();
1808
+ }
1809
+ api?.emit("playlist:reorder", { tracks: [...tracks] });
1810
+ persistPlaylist();
1811
+ },
1812
+ getState() {
1813
+ return {
1814
+ tracks: [...tracks],
1815
+ currentIndex,
1816
+ currentTrack: currentIndex >= 0 ? tracks[currentIndex] : null,
1817
+ shuffle,
1818
+ repeat,
1819
+ shuffleOrder: [...shuffleOrder],
1820
+ hasNext: hasNextTrack(),
1821
+ hasPrevious: hasPreviousTrack()
1822
+ };
1823
+ },
1824
+ getTracks() {
1825
+ return [...tracks];
1826
+ },
1827
+ getCurrentTrack() {
1828
+ return currentIndex >= 0 ? tracks[currentIndex] : null;
1829
+ },
1830
+ getTrack(id) {
1831
+ return tracks.find((t) => t.id === id) || null;
1832
+ }
1833
+ };
1834
+ return plugin;
1835
+ }
1836
+ var DEFAULT_CONFIG = {
1837
+ enablePlayPause: true,
1838
+ enableSeek: true,
1839
+ enableTrackNavigation: true,
1840
+ seekOffset: 10,
1841
+ updatePositionState: true
1842
+ };
1843
+ function isMediaSessionSupported() {
1844
+ return typeof navigator !== "undefined" && "mediaSession" in navigator;
1845
+ }
1846
+ function createMediaSessionPlugin(config) {
1847
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1848
+ let api = null;
1849
+ let currentMetadata = {};
1850
+ const updateMetadata = () => {
1851
+ if (!isMediaSessionSupported()) return;
1852
+ const artwork = [];
1853
+ const artworkSources = currentMetadata.artwork || mergedConfig.defaultArtwork || [];
1854
+ for (const art of artworkSources) {
1855
+ artwork.push({
1856
+ src: art.src,
1857
+ sizes: art.sizes || "512x512",
1858
+ type: art.type || "image/png"
1859
+ });
1860
+ }
1861
+ try {
1862
+ navigator.mediaSession.metadata = new MediaMetadata({
1863
+ title: currentMetadata.title || "Unknown",
1864
+ artist: currentMetadata.artist || "",
1865
+ album: currentMetadata.album || "",
1866
+ artwork
1867
+ });
1868
+ } catch (e) {
1869
+ api?.logger.warn("Failed to set media session metadata", e);
1870
+ }
1871
+ };
1872
+ const updatePositionState = () => {
1873
+ if (!isMediaSessionSupported() || !mergedConfig.updatePositionState) return;
1874
+ const duration = api?.getState("duration") || 0;
1875
+ const position = api?.getState("currentTime") || 0;
1876
+ const playbackRate = api?.getState("playbackRate") || 1;
1877
+ if (duration > 0 && isFinite(duration)) {
1878
+ try {
1879
+ navigator.mediaSession.setPositionState({
1880
+ duration,
1881
+ position: Math.min(position, duration),
1882
+ playbackRate
1883
+ });
1884
+ } catch (e) {
1885
+ api?.logger.debug("Failed to set position state", e);
1886
+ }
1887
+ }
1888
+ };
1889
+ const setupActionHandlers = () => {
1890
+ if (!isMediaSessionSupported()) return;
1891
+ const seekOffset = mergedConfig.seekOffset || 10;
1892
+ if (mergedConfig.enablePlayPause) {
1893
+ try {
1894
+ navigator.mediaSession.setActionHandler("play", () => {
1895
+ api?.logger.debug("Media session: play");
1896
+ api?.emit("playback:play", void 0);
1897
+ });
1898
+ navigator.mediaSession.setActionHandler("pause", () => {
1899
+ api?.logger.debug("Media session: pause");
1900
+ api?.emit("playback:pause", void 0);
1901
+ });
1902
+ navigator.mediaSession.setActionHandler("stop", () => {
1903
+ api?.logger.debug("Media session: stop");
1904
+ api?.emit("playback:pause", void 0);
1905
+ api?.emit("playback:seeking", { time: 0 });
1906
+ });
1907
+ } catch (e) {
1908
+ api?.logger.debug("Some play/pause actions not supported", e);
1909
+ }
1910
+ }
1911
+ if (mergedConfig.enableSeek) {
1912
+ try {
1913
+ navigator.mediaSession.setActionHandler("seekbackward", (details) => {
1914
+ const offset = details.seekOffset || seekOffset;
1915
+ const currentTime = api?.getState("currentTime") || 0;
1916
+ const newTime = Math.max(0, currentTime - offset);
1917
+ api?.logger.debug("Media session: seekbackward", { offset, newTime });
1918
+ api?.emit("playback:seeking", { time: newTime });
1919
+ });
1920
+ navigator.mediaSession.setActionHandler("seekforward", (details) => {
1921
+ const offset = details.seekOffset || seekOffset;
1922
+ const currentTime = api?.getState("currentTime") || 0;
1923
+ const duration = api?.getState("duration") || 0;
1924
+ const newTime = Math.min(duration, currentTime + offset);
1925
+ api?.logger.debug("Media session: seekforward", { offset, newTime });
1926
+ api?.emit("playback:seeking", { time: newTime });
1927
+ });
1928
+ navigator.mediaSession.setActionHandler("seekto", (details) => {
1929
+ if (details.seekTime !== void 0) {
1930
+ api?.logger.debug("Media session: seekto", { time: details.seekTime });
1931
+ api?.emit("playback:seeking", { time: details.seekTime });
1932
+ }
1933
+ });
1934
+ } catch (e) {
1935
+ api?.logger.debug("Some seek actions not supported", e);
1936
+ }
1937
+ }
1938
+ if (mergedConfig.enableTrackNavigation) {
1939
+ try {
1940
+ navigator.mediaSession.setActionHandler("previoustrack", () => {
1941
+ api?.logger.debug("Media session: previoustrack");
1942
+ const playlist = api?.getPlugin("playlist");
1943
+ if (playlist) {
1944
+ playlist.previous();
1945
+ } else {
1946
+ api?.emit("playback:seeking", { time: 0 });
1947
+ }
1948
+ });
1949
+ navigator.mediaSession.setActionHandler("nexttrack", () => {
1950
+ api?.logger.debug("Media session: nexttrack");
1951
+ const playlist = api?.getPlugin("playlist");
1952
+ if (playlist) {
1953
+ playlist.next();
1954
+ }
1955
+ });
1956
+ } catch (e) {
1957
+ api?.logger.debug("Track navigation not supported", e);
1958
+ }
1959
+ }
1960
+ };
1961
+ const clearActionHandlers = () => {
1962
+ if (!isMediaSessionSupported()) return;
1963
+ const actions = [
1964
+ "play",
1965
+ "pause",
1966
+ "stop",
1967
+ "seekbackward",
1968
+ "seekforward",
1969
+ "seekto",
1970
+ "previoustrack",
1971
+ "nexttrack"
1972
+ ];
1973
+ for (const action of actions) {
1974
+ try {
1975
+ navigator.mediaSession.setActionHandler(action, null);
1976
+ } catch (e) {
1977
+ }
1978
+ }
1979
+ };
1980
+ const plugin = {
1981
+ id: "media-session",
1982
+ name: "Media Session",
1983
+ version: "1.0.0",
1984
+ type: "feature",
1985
+ description: "Media Session API integration for system-level media controls",
1986
+ async init(pluginApi) {
1987
+ api = pluginApi;
1988
+ if (!isMediaSessionSupported()) {
1989
+ api.logger.info("Media Session API not supported in this browser");
1990
+ return;
1991
+ }
1992
+ api.logger.info("Media Session plugin initialized");
1993
+ setupActionHandlers();
1994
+ const unsubPlay = api.on("playback:play", () => {
1995
+ if (isMediaSessionSupported()) {
1996
+ navigator.mediaSession.playbackState = "playing";
1997
+ }
1998
+ });
1999
+ const unsubPause = api.on("playback:pause", () => {
2000
+ if (isMediaSessionSupported()) {
2001
+ navigator.mediaSession.playbackState = "paused";
2002
+ }
2003
+ });
2004
+ const unsubEnded = api.on("playback:ended", () => {
2005
+ if (isMediaSessionSupported()) {
2006
+ navigator.mediaSession.playbackState = "none";
2007
+ }
2008
+ });
2009
+ let lastPositionUpdate = 0;
2010
+ const unsubTimeUpdate = api.on("playback:timeupdate", () => {
2011
+ const now = Date.now();
2012
+ if (now - lastPositionUpdate >= 1e3) {
2013
+ lastPositionUpdate = now;
2014
+ updatePositionState();
2015
+ }
2016
+ });
2017
+ const unsubMetadata = api.on("media:loadedmetadata", () => {
2018
+ updatePositionState();
2019
+ });
2020
+ const unsubState = api.subscribeToState((event) => {
2021
+ if (event.key === "title" && typeof event.value === "string") {
2022
+ currentMetadata.title = event.value;
2023
+ updateMetadata();
2024
+ } else if (event.key === "poster" && typeof event.value === "string") {
2025
+ currentMetadata.artwork = [{ src: event.value, sizes: "512x512" }];
2026
+ updateMetadata();
2027
+ }
2028
+ });
2029
+ const unsubPlaylist = api.on("playlist:change", (payload) => {
2030
+ if (payload?.track) {
2031
+ const track = payload.track;
2032
+ currentMetadata = {
2033
+ title: track.title,
2034
+ artist: track.artist,
2035
+ album: track.album,
2036
+ artwork: track.artwork ? [{ src: track.artwork, sizes: "512x512" }] : void 0
2037
+ };
2038
+ updateMetadata();
2039
+ }
2040
+ });
2041
+ api.onDestroy(() => {
2042
+ unsubPlay();
2043
+ unsubPause();
2044
+ unsubEnded();
2045
+ unsubTimeUpdate();
2046
+ unsubMetadata();
2047
+ unsubState();
2048
+ unsubPlaylist();
2049
+ clearActionHandlers();
2050
+ });
2051
+ },
2052
+ async destroy() {
2053
+ api?.logger.info("Media Session plugin destroying");
2054
+ clearActionHandlers();
2055
+ if (isMediaSessionSupported()) {
2056
+ navigator.mediaSession.metadata = null;
2057
+ navigator.mediaSession.playbackState = "none";
2058
+ }
2059
+ api = null;
2060
+ },
2061
+ isSupported() {
2062
+ return isMediaSessionSupported();
2063
+ },
2064
+ setMetadata(metadata) {
2065
+ currentMetadata = { ...currentMetadata, ...metadata };
2066
+ updateMetadata();
2067
+ },
2068
+ setPlaybackState(state) {
2069
+ if (isMediaSessionSupported()) {
2070
+ navigator.mediaSession.playbackState = state;
2071
+ }
2072
+ },
2073
+ setPositionState(state) {
2074
+ if (isMediaSessionSupported()) {
2075
+ try {
2076
+ navigator.mediaSession.setPositionState(state);
2077
+ } catch (e) {
2078
+ api?.logger.debug("Failed to set position state", e);
2079
+ }
2080
+ }
2081
+ },
2082
+ setActionHandler(action, handler) {
2083
+ if (isMediaSessionSupported()) {
2084
+ try {
2085
+ navigator.mediaSession.setActionHandler(action, handler);
2086
+ } catch (e) {
2087
+ api?.logger.debug(`Action ${action} not supported`, e);
2088
+ }
2089
+ }
2090
+ }
2091
+ };
2092
+ return plugin;
2093
+ }
2094
+ class Signal {
2095
+ constructor(initialValue) {
2096
+ this.subscribers = /* @__PURE__ */ new Set();
2097
+ this.value = initialValue;
2098
+ }
2099
+ /**
2100
+ * Get the current value and track dependency if called within an effect.
2101
+ *
2102
+ * @returns Current value
2103
+ */
2104
+ get() {
2105
+ return this.value;
2106
+ }
2107
+ /**
2108
+ * Set a new value and notify subscribers if changed.
2109
+ *
2110
+ * @param newValue - New value to set
2111
+ */
2112
+ set(newValue) {
2113
+ if (Object.is(this.value, newValue)) {
2114
+ return;
2115
+ }
2116
+ this.value = newValue;
2117
+ this.notify();
2118
+ }
2119
+ /**
2120
+ * Update the value using a function.
2121
+ *
2122
+ * @param updater - Function that receives current value and returns new value
2123
+ *
2124
+ * @example
2125
+ * ```ts
2126
+ * const count = new Signal(0);
2127
+ * count.update(n => n + 1); // Increments by 1
2128
+ * ```
2129
+ */
2130
+ update(updater) {
2131
+ this.set(updater(this.value));
2132
+ }
2133
+ /**
2134
+ * Subscribe to changes without automatic dependency tracking.
2135
+ *
2136
+ * @param callback - Function to call when value changes
2137
+ * @returns Unsubscribe function
2138
+ */
2139
+ subscribe(callback) {
2140
+ this.subscribers.add(callback);
2141
+ return () => this.subscribers.delete(callback);
2142
+ }
2143
+ /**
2144
+ * Notify all subscribers of a change.
2145
+ * @internal
2146
+ */
2147
+ notify() {
2148
+ this.subscribers.forEach((subscriber) => {
2149
+ try {
2150
+ subscriber();
2151
+ } catch (error) {
2152
+ console.error("[Scarlett Player] Error in signal subscriber:", error);
2153
+ }
2154
+ });
2155
+ }
2156
+ /**
2157
+ * Clean up all subscriptions.
2158
+ * Call this when destroying the signal.
2159
+ */
2160
+ destroy() {
2161
+ this.subscribers.clear();
2162
+ }
2163
+ /**
2164
+ * Get the current number of subscribers (for debugging).
2165
+ * @internal
2166
+ */
2167
+ getSubscriberCount() {
2168
+ return this.subscribers.size;
2169
+ }
2170
+ }
2171
+ function signal(initialValue) {
2172
+ return new Signal(initialValue);
2173
+ }
2174
+ const DEFAULT_STATE = {
2175
+ // Core Playback State
2176
+ playbackState: "idle",
2177
+ playing: false,
2178
+ paused: true,
2179
+ ended: false,
2180
+ buffering: false,
2181
+ waiting: false,
2182
+ seeking: false,
2183
+ // Time & Duration
2184
+ currentTime: 0,
2185
+ duration: NaN,
2186
+ buffered: null,
2187
+ bufferedAmount: 0,
2188
+ // Media Info
2189
+ mediaType: "unknown",
2190
+ source: null,
2191
+ title: "",
2192
+ poster: "",
2193
+ // Volume & Audio
2194
+ volume: 1,
2195
+ muted: false,
2196
+ // Playback Controls
2197
+ playbackRate: 1,
2198
+ fullscreen: false,
2199
+ pip: false,
2200
+ controlsVisible: true,
2201
+ // Quality & Tracks
2202
+ qualities: [],
2203
+ currentQuality: null,
2204
+ audioTracks: [],
2205
+ currentAudioTrack: null,
2206
+ textTracks: [],
2207
+ currentTextTrack: null,
2208
+ // Live/DVR State (TSP features)
2209
+ live: false,
2210
+ liveEdge: true,
2211
+ seekableRange: null,
2212
+ liveLatency: 0,
2213
+ lowLatencyMode: false,
2214
+ // Chapters (TSP features)
2215
+ chapters: [],
2216
+ currentChapter: null,
2217
+ // Error State
2218
+ error: null,
2219
+ // Network & Performance
2220
+ bandwidth: 0,
2221
+ autoplay: false,
2222
+ loop: false,
2223
+ // Casting State
2224
+ airplayAvailable: false,
2225
+ airplayActive: false,
2226
+ chromecastAvailable: false,
2227
+ chromecastActive: false,
2228
+ // UI State
2229
+ interacting: false,
2230
+ hovering: false,
2231
+ focused: false
2232
+ };
2233
+ class StateManager {
2234
+ /**
2235
+ * Create a new StateManager with default initial state.
2236
+ *
2237
+ * @param initialState - Optional partial initial state (merged with defaults)
2238
+ */
2239
+ constructor(initialState) {
2240
+ this.signals = /* @__PURE__ */ new Map();
2241
+ this.changeSubscribers = /* @__PURE__ */ new Set();
2242
+ this.initializeSignals(initialState);
2243
+ }
2244
+ /**
2245
+ * Initialize all state signals with default or provided values.
2246
+ * @private
2247
+ */
2248
+ initializeSignals(overrides) {
2249
+ const initialState = { ...DEFAULT_STATE, ...overrides };
2250
+ for (const [key, value] of Object.entries(initialState)) {
2251
+ const stateKey = key;
2252
+ const stateSignal = signal(value);
2253
+ stateSignal.subscribe(() => {
2254
+ this.notifyChangeSubscribers(stateKey);
2255
+ });
2256
+ this.signals.set(stateKey, stateSignal);
2257
+ }
2258
+ }
2259
+ /**
2260
+ * Get the signal for a state property.
2261
+ *
2262
+ * @param key - State property key
2263
+ * @returns Signal for the property
2264
+ *
2265
+ * @example
2266
+ * ```ts
2267
+ * const playingSignal = state.get('playing');
2268
+ * playingSignal.get(); // false
2269
+ * playingSignal.set(true);
2270
+ * ```
2271
+ */
2272
+ get(key) {
2273
+ const stateSignal = this.signals.get(key);
2274
+ if (!stateSignal) {
2275
+ throw new Error(`[StateManager] Unknown state key: ${key}`);
2276
+ }
2277
+ return stateSignal;
2278
+ }
2279
+ /**
2280
+ * Get the current value of a state property (convenience method).
2281
+ *
2282
+ * @param key - State property key
2283
+ * @returns Current value
2284
+ *
2285
+ * @example
2286
+ * ```ts
2287
+ * state.getValue('playing'); // false
2288
+ * ```
2289
+ */
2290
+ getValue(key) {
2291
+ return this.get(key).get();
2292
+ }
2293
+ /**
2294
+ * Set the value of a state property.
2295
+ *
2296
+ * @param key - State property key
2297
+ * @param value - New value
2298
+ *
2299
+ * @example
2300
+ * ```ts
2301
+ * state.set('playing', true);
2302
+ * state.set('currentTime', 10.5);
2303
+ * ```
2304
+ */
2305
+ set(key, value) {
2306
+ this.get(key).set(value);
2307
+ }
2308
+ /**
2309
+ * Update multiple state properties at once (batch update).
2310
+ *
2311
+ * More efficient than calling set() multiple times.
2312
+ *
2313
+ * @param updates - Partial state object with updates
2314
+ *
2315
+ * @example
2316
+ * ```ts
2317
+ * state.update({
2318
+ * playing: true,
2319
+ * currentTime: 0,
2320
+ * volume: 1.0,
2321
+ * });
2322
+ * ```
2323
+ */
2324
+ update(updates) {
2325
+ for (const [key, value] of Object.entries(updates)) {
2326
+ const stateKey = key;
2327
+ if (this.signals.has(stateKey)) {
2328
+ this.set(stateKey, value);
2329
+ }
2330
+ }
2331
+ }
2332
+ /**
2333
+ * Subscribe to changes on a specific state property.
2334
+ *
2335
+ * @param key - State property key
2336
+ * @param callback - Callback function receiving new value
2337
+ * @returns Unsubscribe function
2338
+ *
2339
+ * @example
2340
+ * ```ts
2341
+ * const unsub = state.subscribe('playing', (value) => {
2342
+ * console.log('Playing:', value);
2343
+ * });
2344
+ * ```
2345
+ */
2346
+ subscribeToKey(key, callback) {
2347
+ const stateSignal = this.get(key);
2348
+ return stateSignal.subscribe(() => {
2349
+ callback(stateSignal.get());
2350
+ });
2351
+ }
2352
+ /**
2353
+ * Subscribe to all state changes.
2354
+ *
2355
+ * Receives a StateChangeEvent for every state property change.
2356
+ *
2357
+ * @param callback - Callback function receiving change events
2358
+ * @returns Unsubscribe function
2359
+ *
2360
+ * @example
2361
+ * ```ts
2362
+ * const unsub = state.subscribe((event) => {
2363
+ * console.log(`${event.key} changed:`, event.value);
2364
+ * });
2365
+ * ```
2366
+ */
2367
+ subscribe(callback) {
2368
+ this.changeSubscribers.add(callback);
2369
+ return () => this.changeSubscribers.delete(callback);
2370
+ }
2371
+ /**
2372
+ * Notify all global change subscribers.
2373
+ * @private
2374
+ */
2375
+ notifyChangeSubscribers(key) {
2376
+ const stateSignal = this.get(key);
2377
+ const value = stateSignal.get();
2378
+ const event = {
2379
+ key,
2380
+ value,
2381
+ previousValue: value
2382
+ // Note: We don't track previous values in this simple impl
2383
+ };
2384
+ this.changeSubscribers.forEach((subscriber) => {
2385
+ try {
2386
+ subscriber(event);
2387
+ } catch (error) {
2388
+ console.error("[StateManager] Error in change subscriber:", error);
2389
+ }
2390
+ });
2391
+ }
2392
+ /**
2393
+ * Reset all state to default values.
2394
+ *
2395
+ * @example
2396
+ * ```ts
2397
+ * state.reset();
2398
+ * ```
2399
+ */
2400
+ reset() {
2401
+ this.update(DEFAULT_STATE);
2402
+ }
2403
+ /**
2404
+ * Reset a specific state property to its default value.
2405
+ *
2406
+ * @param key - State property key
2407
+ *
2408
+ * @example
2409
+ * ```ts
2410
+ * state.resetKey('playing');
2411
+ * ```
2412
+ */
2413
+ resetKey(key) {
2414
+ const defaultValue = DEFAULT_STATE[key];
2415
+ this.set(key, defaultValue);
2416
+ }
2417
+ /**
2418
+ * Get a snapshot of all current state values.
2419
+ *
2420
+ * @returns Frozen snapshot of current state
2421
+ *
2422
+ * @example
2423
+ * ```ts
2424
+ * const snapshot = state.snapshot();
2425
+ * console.log(snapshot.playing, snapshot.currentTime);
2426
+ * ```
2427
+ */
2428
+ snapshot() {
2429
+ const snapshot = {};
2430
+ for (const [key, stateSignal] of this.signals) {
2431
+ snapshot[key] = stateSignal.get();
2432
+ }
2433
+ return Object.freeze(snapshot);
2434
+ }
2435
+ /**
2436
+ * Get the number of subscribers for a state property (for debugging).
2437
+ *
2438
+ * @param key - State property key
2439
+ * @returns Number of subscribers
2440
+ * @internal
2441
+ */
2442
+ getSubscriberCount(key) {
2443
+ return this.signals.get(key)?.getSubscriberCount() ?? 0;
2444
+ }
2445
+ /**
2446
+ * Destroy the state manager and cleanup all signals.
2447
+ *
2448
+ * @example
2449
+ * ```ts
2450
+ * state.destroy();
2451
+ * ```
2452
+ */
2453
+ destroy() {
2454
+ this.signals.forEach((stateSignal) => stateSignal.destroy());
2455
+ this.signals.clear();
2456
+ this.changeSubscribers.clear();
2457
+ }
2458
+ }
2459
+ const DEFAULT_OPTIONS = {
2460
+ maxListeners: 100,
2461
+ async: false,
2462
+ interceptors: true
2463
+ };
2464
+ class EventBus {
2465
+ /**
2466
+ * Create a new EventBus.
2467
+ *
2468
+ * @param options - Optional configuration
2469
+ */
2470
+ constructor(options) {
2471
+ this.listeners = /* @__PURE__ */ new Map();
2472
+ this.onceListeners = /* @__PURE__ */ new Map();
2473
+ this.interceptors = /* @__PURE__ */ new Map();
2474
+ this.options = { ...DEFAULT_OPTIONS, ...options };
2475
+ }
2476
+ /**
2477
+ * Subscribe to an event.
2478
+ *
2479
+ * @param event - Event name
2480
+ * @param handler - Event handler function
2481
+ * @returns Unsubscribe function
2482
+ *
2483
+ * @example
2484
+ * ```ts
2485
+ * const unsub = events.on('playback:play', () => {
2486
+ * console.log('Playing!');
2487
+ * });
2488
+ *
2489
+ * // Later: unsubscribe
2490
+ * unsub();
2491
+ * ```
2492
+ */
2493
+ on(event, handler) {
2494
+ if (!this.listeners.has(event)) {
2495
+ this.listeners.set(event, /* @__PURE__ */ new Set());
2496
+ }
2497
+ const handlers = this.listeners.get(event);
2498
+ handlers.add(handler);
2499
+ this.checkMaxListeners(event);
2500
+ return () => this.off(event, handler);
2501
+ }
2502
+ /**
2503
+ * Subscribe to an event once (auto-unsubscribe after first call).
2504
+ *
2505
+ * @param event - Event name
2506
+ * @param handler - Event handler function
2507
+ * @returns Unsubscribe function
2508
+ *
2509
+ * @example
2510
+ * ```ts
2511
+ * events.once('player:ready', () => {
2512
+ * console.log('Player ready!');
2513
+ * });
2514
+ * ```
2515
+ */
2516
+ once(event, handler) {
2517
+ if (!this.onceListeners.has(event)) {
2518
+ this.onceListeners.set(event, /* @__PURE__ */ new Set());
2519
+ }
2520
+ const handlers = this.onceListeners.get(event);
2521
+ handlers.add(handler);
2522
+ if (!this.listeners.has(event)) {
2523
+ this.listeners.set(event, /* @__PURE__ */ new Set());
2524
+ }
2525
+ return () => {
2526
+ handlers.delete(handler);
2527
+ };
2528
+ }
2529
+ /**
2530
+ * Unsubscribe from an event.
2531
+ *
2532
+ * @param event - Event name
2533
+ * @param handler - Event handler function to remove
2534
+ *
2535
+ * @example
2536
+ * ```ts
2537
+ * const handler = () => console.log('Playing!');
2538
+ * events.on('playback:play', handler);
2539
+ * events.off('playback:play', handler);
2540
+ * ```
2541
+ */
2542
+ off(event, handler) {
2543
+ const handlers = this.listeners.get(event);
2544
+ if (handlers) {
2545
+ handlers.delete(handler);
2546
+ if (handlers.size === 0) {
2547
+ this.listeners.delete(event);
2548
+ }
2549
+ }
2550
+ const onceHandlers = this.onceListeners.get(event);
2551
+ if (onceHandlers) {
2552
+ onceHandlers.delete(handler);
2553
+ if (onceHandlers.size === 0) {
2554
+ this.onceListeners.delete(event);
2555
+ }
2556
+ }
2557
+ }
2558
+ /**
2559
+ * Emit an event synchronously.
2560
+ *
2561
+ * Runs interceptors first, then calls all handlers.
2562
+ *
2563
+ * @param event - Event name
2564
+ * @param payload - Event payload
2565
+ *
2566
+ * @example
2567
+ * ```ts
2568
+ * events.emit('playback:play', undefined);
2569
+ * events.emit('playback:timeupdate', { currentTime: 10.5 });
2570
+ * ```
2571
+ */
2572
+ emit(event, payload) {
2573
+ const interceptedPayload = this.runInterceptors(event, payload);
2574
+ if (interceptedPayload === null) {
2575
+ return;
2576
+ }
2577
+ const handlers = this.listeners.get(event);
2578
+ if (handlers) {
2579
+ const handlersArray = Array.from(handlers);
2580
+ handlersArray.forEach((handler) => {
2581
+ this.safeCallHandler(handler, interceptedPayload);
2582
+ });
2583
+ }
2584
+ const onceHandlers = this.onceListeners.get(event);
2585
+ if (onceHandlers) {
2586
+ const handlersArray = Array.from(onceHandlers);
2587
+ handlersArray.forEach((handler) => {
2588
+ this.safeCallHandler(handler, interceptedPayload);
2589
+ });
2590
+ this.onceListeners.delete(event);
2591
+ }
2592
+ }
2593
+ /**
2594
+ * Emit an event asynchronously (next tick).
2595
+ *
2596
+ * @param event - Event name
2597
+ * @param payload - Event payload
2598
+ * @returns Promise that resolves when all handlers complete
2599
+ *
2600
+ * @example
2601
+ * ```ts
2602
+ * await events.emitAsync('media:loaded', { src: 'video.mp4', type: 'video/mp4' });
2603
+ * ```
2604
+ */
2605
+ async emitAsync(event, payload) {
2606
+ const interceptedPayload = await this.runInterceptorsAsync(event, payload);
2607
+ if (interceptedPayload === null) {
2608
+ return;
2609
+ }
2610
+ const handlers = this.listeners.get(event);
2611
+ if (handlers) {
2612
+ const promises = Array.from(handlers).map(
2613
+ (handler) => this.safeCallHandlerAsync(handler, interceptedPayload)
2614
+ );
2615
+ await Promise.all(promises);
2616
+ }
2617
+ const onceHandlers = this.onceListeners.get(event);
2618
+ if (onceHandlers) {
2619
+ const handlersArray = Array.from(onceHandlers);
2620
+ const promises = handlersArray.map(
2621
+ (handler) => this.safeCallHandlerAsync(handler, interceptedPayload)
2622
+ );
2623
+ await Promise.all(promises);
2624
+ this.onceListeners.delete(event);
2625
+ }
2626
+ }
2627
+ /**
2628
+ * Add an event interceptor.
2629
+ *
2630
+ * Interceptors run before handlers and can modify or cancel events.
2631
+ *
2632
+ * @param event - Event name
2633
+ * @param interceptor - Interceptor function
2634
+ * @returns Remove interceptor function
2635
+ *
2636
+ * @example
2637
+ * ```ts
2638
+ * events.intercept('playback:timeupdate', (payload) => {
2639
+ * // Round time to 2 decimals
2640
+ * return { currentTime: Math.round(payload.currentTime * 100) / 100 };
2641
+ * });
2642
+ *
2643
+ * // Cancel events
2644
+ * events.intercept('playback:play', (payload) => {
2645
+ * if (notReady) return null; // Cancel event
2646
+ * return payload;
2647
+ * });
2648
+ * ```
2649
+ */
2650
+ intercept(event, interceptor) {
2651
+ if (!this.options.interceptors) {
2652
+ return () => {
2653
+ };
2654
+ }
2655
+ if (!this.interceptors.has(event)) {
2656
+ this.interceptors.set(event, /* @__PURE__ */ new Set());
2657
+ }
2658
+ const interceptorsSet = this.interceptors.get(event);
2659
+ interceptorsSet.add(interceptor);
2660
+ return () => {
2661
+ interceptorsSet.delete(interceptor);
2662
+ if (interceptorsSet.size === 0) {
2663
+ this.interceptors.delete(event);
2664
+ }
2665
+ };
2666
+ }
2667
+ /**
2668
+ * Remove all listeners for an event (or all events if no event specified).
2669
+ *
2670
+ * @param event - Optional event name
2671
+ *
2672
+ * @example
2673
+ * ```ts
2674
+ * events.removeAllListeners('playback:play'); // Remove all playback:play listeners
2675
+ * events.removeAllListeners(); // Remove ALL listeners
2676
+ * ```
2677
+ */
2678
+ removeAllListeners(event) {
2679
+ if (event) {
2680
+ this.listeners.delete(event);
2681
+ this.onceListeners.delete(event);
2682
+ } else {
2683
+ this.listeners.clear();
2684
+ this.onceListeners.clear();
2685
+ }
2686
+ }
2687
+ /**
2688
+ * Get the number of listeners for an event.
2689
+ *
2690
+ * @param event - Event name
2691
+ * @returns Number of listeners
2692
+ *
2693
+ * @example
2694
+ * ```ts
2695
+ * events.listenerCount('playback:play'); // 3
2696
+ * ```
2697
+ */
2698
+ listenerCount(event) {
2699
+ const regularCount = this.listeners.get(event)?.size ?? 0;
2700
+ const onceCount = this.onceListeners.get(event)?.size ?? 0;
2701
+ return regularCount + onceCount;
2702
+ }
2703
+ /**
2704
+ * Destroy event bus and cleanup all listeners/interceptors.
2705
+ *
2706
+ * @example
2707
+ * ```ts
2708
+ * events.destroy();
2709
+ * ```
2710
+ */
2711
+ destroy() {
2712
+ this.listeners.clear();
2713
+ this.onceListeners.clear();
2714
+ this.interceptors.clear();
2715
+ }
2716
+ /**
2717
+ * Run interceptors synchronously.
2718
+ * @private
2719
+ */
2720
+ runInterceptors(event, payload) {
2721
+ if (!this.options.interceptors) {
2722
+ return payload;
2723
+ }
2724
+ const interceptorsSet = this.interceptors.get(event);
2725
+ if (!interceptorsSet || interceptorsSet.size === 0) {
2726
+ return payload;
2727
+ }
2728
+ let currentPayload = payload;
2729
+ for (const interceptor of interceptorsSet) {
2730
+ try {
2731
+ currentPayload = interceptor(currentPayload);
2732
+ if (currentPayload === null) {
2733
+ return null;
2734
+ }
2735
+ } catch (error) {
2736
+ console.error("[EventBus] Error in interceptor:", error);
2737
+ }
2738
+ }
2739
+ return currentPayload;
2740
+ }
2741
+ /**
2742
+ * Run interceptors asynchronously.
2743
+ * @private
2744
+ */
2745
+ async runInterceptorsAsync(event, payload) {
2746
+ if (!this.options.interceptors) {
2747
+ return payload;
2748
+ }
2749
+ const interceptorsSet = this.interceptors.get(event);
2750
+ if (!interceptorsSet || interceptorsSet.size === 0) {
2751
+ return payload;
2752
+ }
2753
+ let currentPayload = payload;
2754
+ for (const interceptor of interceptorsSet) {
2755
+ try {
2756
+ const result = interceptor(currentPayload);
2757
+ currentPayload = result instanceof Promise ? await result : result;
2758
+ if (currentPayload === null) {
2759
+ return null;
2760
+ }
2761
+ } catch (error) {
2762
+ console.error("[EventBus] Error in interceptor:", error);
2763
+ }
2764
+ }
2765
+ return currentPayload;
2766
+ }
2767
+ /**
2768
+ * Safely call a handler with error handling.
2769
+ * @private
2770
+ */
2771
+ safeCallHandler(handler, payload) {
2772
+ try {
2773
+ handler(payload);
2774
+ } catch (error) {
2775
+ console.error("[EventBus] Error in event handler:", error);
2776
+ }
2777
+ }
2778
+ /**
2779
+ * Safely call a handler asynchronously with error handling.
2780
+ * @private
2781
+ */
2782
+ async safeCallHandlerAsync(handler, payload) {
2783
+ try {
2784
+ const result = handler(payload);
2785
+ if (result instanceof Promise) {
2786
+ await result;
2787
+ }
2788
+ } catch (error) {
2789
+ console.error("[EventBus] Error in event handler:", error);
2790
+ }
2791
+ }
2792
+ /**
2793
+ * Check if max listeners exceeded and warn.
2794
+ * @private
2795
+ */
2796
+ checkMaxListeners(event) {
2797
+ const count = this.listenerCount(event);
2798
+ if (count > this.options.maxListeners) {
2799
+ console.warn(
2800
+ `[EventBus] Max listeners (${this.options.maxListeners}) exceeded for event: ${event}. Current count: ${count}. This may indicate a memory leak.`
2801
+ );
2802
+ }
2803
+ }
2804
+ }
2805
+ const LOG_LEVELS = ["debug", "info", "warn", "error"];
2806
+ const defaultConsoleHandler = (entry) => {
2807
+ const prefix = entry.scope ? `[${entry.scope}]` : "[ScarlettPlayer]";
2808
+ const message = `${prefix} ${entry.message}`;
2809
+ const metadata = entry.metadata ?? "";
2810
+ switch (entry.level) {
2811
+ case "debug":
2812
+ console.debug(message, metadata);
2813
+ break;
2814
+ case "info":
2815
+ console.info(message, metadata);
2816
+ break;
2817
+ case "warn":
2818
+ console.warn(message, metadata);
2819
+ break;
2820
+ case "error":
2821
+ console.error(message, metadata);
2822
+ break;
2823
+ }
2824
+ };
2825
+ class Logger {
2826
+ /**
2827
+ * Create a new Logger.
2828
+ *
2829
+ * @param options - Logger configuration
2830
+ */
2831
+ constructor(options) {
2832
+ this.level = options?.level ?? "warn";
2833
+ this.scope = options?.scope;
2834
+ this.enabled = options?.enabled ?? true;
2835
+ this.handlers = options?.handlers ?? [defaultConsoleHandler];
2836
+ }
2837
+ /**
2838
+ * Create a child logger with a scope.
2839
+ *
2840
+ * Child loggers inherit parent settings and chain scopes.
2841
+ *
2842
+ * @param scope - Child logger scope
2843
+ * @returns New child logger
2844
+ *
2845
+ * @example
2846
+ * ```ts
2847
+ * const logger = new Logger();
2848
+ * const hlsLogger = logger.child('hls-plugin');
2849
+ * hlsLogger.info('Loading manifest');
2850
+ * // Output: [ScarlettPlayer:hls-plugin] Loading manifest
2851
+ * ```
2852
+ */
2853
+ child(scope) {
2854
+ return new Logger({
2855
+ level: this.level,
2856
+ scope: this.scope ? `${this.scope}:${scope}` : scope,
2857
+ enabled: this.enabled,
2858
+ handlers: this.handlers
2859
+ });
2860
+ }
2861
+ /**
2862
+ * Log a debug message.
2863
+ *
2864
+ * @param message - Log message
2865
+ * @param metadata - Optional structured metadata
2866
+ *
2867
+ * @example
2868
+ * ```ts
2869
+ * logger.debug('Request sent', { url: '/api/video' });
2870
+ * ```
2871
+ */
2872
+ debug(message, metadata) {
2873
+ this.log("debug", message, metadata);
2874
+ }
2875
+ /**
2876
+ * Log an info message.
2877
+ *
2878
+ * @param message - Log message
2879
+ * @param metadata - Optional structured metadata
2880
+ *
2881
+ * @example
2882
+ * ```ts
2883
+ * logger.info('Player ready');
2884
+ * ```
2885
+ */
2886
+ info(message, metadata) {
2887
+ this.log("info", message, metadata);
2888
+ }
2889
+ /**
2890
+ * Log a warning message.
2891
+ *
2892
+ * @param message - Log message
2893
+ * @param metadata - Optional structured metadata
2894
+ *
2895
+ * @example
2896
+ * ```ts
2897
+ * logger.warn('Low buffer', { buffered: 2.5 });
2898
+ * ```
2899
+ */
2900
+ warn(message, metadata) {
2901
+ this.log("warn", message, metadata);
2902
+ }
2903
+ /**
2904
+ * Log an error message.
2905
+ *
2906
+ * @param message - Log message
2907
+ * @param metadata - Optional structured metadata
2908
+ *
2909
+ * @example
2910
+ * ```ts
2911
+ * logger.error('Playback failed', { code: 'MEDIA_ERR_DECODE' });
2912
+ * ```
2913
+ */
2914
+ error(message, metadata) {
2915
+ this.log("error", message, metadata);
2916
+ }
2917
+ /**
2918
+ * Set the minimum log level threshold.
2919
+ *
2920
+ * @param level - New log level
2921
+ *
2922
+ * @example
2923
+ * ```ts
2924
+ * logger.setLevel('debug'); // Show all logs
2925
+ * logger.setLevel('error'); // Show only errors
2926
+ * ```
2927
+ */
2928
+ setLevel(level) {
2929
+ this.level = level;
2930
+ }
2931
+ /**
2932
+ * Enable or disable logging.
2933
+ *
2934
+ * @param enabled - Enable flag
2935
+ *
2936
+ * @example
2937
+ * ```ts
2938
+ * logger.setEnabled(false); // Disable all logging
2939
+ * ```
2940
+ */
2941
+ setEnabled(enabled) {
2942
+ this.enabled = enabled;
2943
+ }
2944
+ /**
2945
+ * Add a custom log handler.
2946
+ *
2947
+ * @param handler - Log handler function
2948
+ *
2949
+ * @example
2950
+ * ```ts
2951
+ * logger.addHandler((entry) => {
2952
+ * if (entry.level === 'error') {
2953
+ * sendToAnalytics(entry);
2954
+ * }
2955
+ * });
2956
+ * ```
2957
+ */
2958
+ addHandler(handler) {
2959
+ this.handlers.push(handler);
2960
+ }
2961
+ /**
2962
+ * Remove a custom log handler.
2963
+ *
2964
+ * @param handler - Log handler function to remove
2965
+ *
2966
+ * @example
2967
+ * ```ts
2968
+ * logger.removeHandler(myHandler);
2969
+ * ```
2970
+ */
2971
+ removeHandler(handler) {
2972
+ const index = this.handlers.indexOf(handler);
2973
+ if (index !== -1) {
2974
+ this.handlers.splice(index, 1);
2975
+ }
2976
+ }
2977
+ /**
2978
+ * Core logging implementation.
2979
+ * @private
2980
+ */
2981
+ log(level, message, metadata) {
2982
+ if (!this.enabled || !this.shouldLog(level)) {
2983
+ return;
2984
+ }
2985
+ const entry = {
2986
+ level,
2987
+ message,
2988
+ timestamp: Date.now(),
2989
+ scope: this.scope,
2990
+ metadata
2991
+ };
2992
+ for (const handler of this.handlers) {
2993
+ try {
2994
+ handler(entry);
2995
+ } catch (error) {
2996
+ console.error("[Logger] Handler error:", error);
2997
+ }
2998
+ }
2999
+ }
3000
+ /**
3001
+ * Check if a log level should be output.
3002
+ * @private
3003
+ */
3004
+ shouldLog(level) {
3005
+ return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.level);
3006
+ }
3007
+ }
3008
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
3009
+ ErrorCode2["SOURCE_NOT_SUPPORTED"] = "SOURCE_NOT_SUPPORTED";
3010
+ ErrorCode2["SOURCE_LOAD_FAILED"] = "SOURCE_LOAD_FAILED";
3011
+ ErrorCode2["PROVIDER_NOT_FOUND"] = "PROVIDER_NOT_FOUND";
3012
+ ErrorCode2["PROVIDER_SETUP_FAILED"] = "PROVIDER_SETUP_FAILED";
3013
+ ErrorCode2["PLUGIN_SETUP_FAILED"] = "PLUGIN_SETUP_FAILED";
3014
+ ErrorCode2["PLUGIN_NOT_FOUND"] = "PLUGIN_NOT_FOUND";
3015
+ ErrorCode2["PLAYBACK_FAILED"] = "PLAYBACK_FAILED";
3016
+ ErrorCode2["MEDIA_DECODE_ERROR"] = "MEDIA_DECODE_ERROR";
3017
+ ErrorCode2["MEDIA_NETWORK_ERROR"] = "MEDIA_NETWORK_ERROR";
3018
+ ErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
3019
+ return ErrorCode2;
3020
+ })(ErrorCode || {});
3021
+ class ErrorHandler {
3022
+ /**
3023
+ * Create a new ErrorHandler.
3024
+ *
3025
+ * @param eventBus - Event bus for error emission
3026
+ * @param logger - Logger for error logging
3027
+ * @param options - Optional configuration
3028
+ */
3029
+ constructor(eventBus, logger, options) {
3030
+ this.errors = [];
3031
+ this.eventBus = eventBus;
3032
+ this.logger = logger;
3033
+ this.maxHistory = options?.maxHistory ?? 10;
3034
+ }
3035
+ /**
3036
+ * Handle an error.
3037
+ *
3038
+ * Normalizes, logs, emits, and tracks the error.
3039
+ *
3040
+ * @param error - Error to handle (native or PlayerError)
3041
+ * @param context - Optional context (what was happening)
3042
+ * @returns Normalized PlayerError
3043
+ *
3044
+ * @example
3045
+ * ```ts
3046
+ * try {
3047
+ * loadVideo();
3048
+ * } catch (error) {
3049
+ * errorHandler.handle(error as Error, { src: 'video.mp4' });
3050
+ * }
3051
+ * ```
3052
+ */
3053
+ handle(error, context) {
3054
+ const playerError = this.normalizeError(error, context);
3055
+ this.addToHistory(playerError);
3056
+ this.logError(playerError);
3057
+ this.eventBus.emit("error", playerError);
3058
+ return playerError;
3059
+ }
3060
+ /**
3061
+ * Create and handle an error from code.
3062
+ *
3063
+ * @param code - Error code
3064
+ * @param message - Error message
3065
+ * @param options - Optional error options
3066
+ * @returns Created PlayerError
3067
+ *
3068
+ * @example
3069
+ * ```ts
3070
+ * errorHandler.throw(
3071
+ * ErrorCode.SOURCE_NOT_SUPPORTED,
3072
+ * 'MP4 not supported',
3073
+ * { fatal: true, context: { type: 'video/mp4' } }
3074
+ * );
3075
+ * ```
3076
+ */
3077
+ throw(code, message, options) {
3078
+ const error = {
3079
+ code,
3080
+ message,
3081
+ fatal: options?.fatal ?? this.isFatalCode(code),
3082
+ timestamp: Date.now(),
3083
+ context: options?.context,
3084
+ originalError: options?.originalError
3085
+ };
3086
+ return this.handle(error, options?.context);
3087
+ }
3088
+ /**
3089
+ * Get error history.
3090
+ *
3091
+ * @returns Readonly copy of error history
3092
+ *
3093
+ * @example
3094
+ * ```ts
3095
+ * const history = errorHandler.getHistory();
3096
+ * console.log(`${history.length} errors occurred`);
3097
+ * ```
3098
+ */
3099
+ getHistory() {
3100
+ return [...this.errors];
3101
+ }
3102
+ /**
3103
+ * Get last error that occurred.
3104
+ *
3105
+ * @returns Last error or null if none
3106
+ *
3107
+ * @example
3108
+ * ```ts
3109
+ * const lastError = errorHandler.getLastError();
3110
+ * if (lastError?.fatal) {
3111
+ * showErrorMessage(lastError.message);
3112
+ * }
3113
+ * ```
3114
+ */
3115
+ getLastError() {
3116
+ return this.errors[this.errors.length - 1] ?? null;
3117
+ }
3118
+ /**
3119
+ * Clear error history.
3120
+ *
3121
+ * @example
3122
+ * ```ts
3123
+ * errorHandler.clearHistory();
3124
+ * ```
3125
+ */
3126
+ clearHistory() {
3127
+ this.errors = [];
3128
+ }
3129
+ /**
3130
+ * Check if any fatal errors occurred.
3131
+ *
3132
+ * @returns True if any fatal error in history
3133
+ *
3134
+ * @example
3135
+ * ```ts
3136
+ * if (errorHandler.hasFatalError()) {
3137
+ * player.reset();
3138
+ * }
3139
+ * ```
3140
+ */
3141
+ hasFatalError() {
3142
+ return this.errors.some((e) => e.fatal);
3143
+ }
3144
+ /**
3145
+ * Normalize error to PlayerError.
3146
+ * @private
3147
+ */
3148
+ normalizeError(error, context) {
3149
+ if (this.isPlayerError(error)) {
3150
+ return {
3151
+ ...error,
3152
+ context: { ...error.context, ...context }
3153
+ };
3154
+ }
3155
+ return {
3156
+ code: this.getErrorCode(error),
3157
+ message: error.message,
3158
+ fatal: this.isFatal(error),
3159
+ timestamp: Date.now(),
3160
+ context,
3161
+ originalError: error
3162
+ };
3163
+ }
3164
+ /**
3165
+ * Determine error code from native Error.
3166
+ * @private
3167
+ */
3168
+ getErrorCode(error) {
3169
+ const message = error.message.toLowerCase();
3170
+ if (message.includes("network")) {
3171
+ return "MEDIA_NETWORK_ERROR";
3172
+ }
3173
+ if (message.includes("decode")) {
3174
+ return "MEDIA_DECODE_ERROR";
3175
+ }
3176
+ if (message.includes("source")) {
3177
+ return "SOURCE_LOAD_FAILED";
3178
+ }
3179
+ if (message.includes("plugin")) {
3180
+ return "PLUGIN_SETUP_FAILED";
3181
+ }
3182
+ if (message.includes("provider")) {
3183
+ return "PROVIDER_SETUP_FAILED";
3184
+ }
3185
+ return "UNKNOWN_ERROR";
3186
+ }
3187
+ /**
3188
+ * Determine if error is fatal.
3189
+ * @private
3190
+ */
3191
+ isFatal(error) {
3192
+ return this.isFatalCode(this.getErrorCode(error));
3193
+ }
3194
+ /**
3195
+ * Determine if error code is fatal.
3196
+ * @private
3197
+ */
3198
+ isFatalCode(code) {
3199
+ const fatalCodes = [
3200
+ "SOURCE_NOT_SUPPORTED",
3201
+ "PROVIDER_NOT_FOUND",
3202
+ "MEDIA_DECODE_ERROR"
3203
+ /* MEDIA_DECODE_ERROR */
3204
+ ];
3205
+ return fatalCodes.includes(code);
3206
+ }
3207
+ /**
3208
+ * Type guard for PlayerError.
3209
+ * @private
3210
+ */
3211
+ isPlayerError(error) {
3212
+ return typeof error === "object" && error !== null && "code" in error && "message" in error && "fatal" in error && "timestamp" in error;
3213
+ }
3214
+ /**
3215
+ * Add error to history.
3216
+ * @private
3217
+ */
3218
+ addToHistory(error) {
3219
+ this.errors.push(error);
3220
+ if (this.errors.length > this.maxHistory) {
3221
+ this.errors.shift();
3222
+ }
3223
+ }
3224
+ /**
3225
+ * Log error with appropriate level.
3226
+ * @private
3227
+ */
3228
+ logError(error) {
3229
+ const logMessage = `[${error.code}] ${error.message}`;
3230
+ if (error.fatal) {
3231
+ this.logger.error(logMessage, {
3232
+ code: error.code,
3233
+ context: error.context
3234
+ });
3235
+ } else {
3236
+ this.logger.warn(logMessage, {
3237
+ code: error.code,
3238
+ context: error.context
3239
+ });
3240
+ }
3241
+ }
3242
+ }
3243
+ class PluginAPI {
3244
+ /**
3245
+ * Create a new PluginAPI.
3246
+ *
3247
+ * @param pluginId - ID of the plugin this API belongs to
3248
+ * @param deps - Dependencies (stateManager, eventBus, logger, container, getPlugin)
3249
+ */
3250
+ constructor(pluginId, deps) {
3251
+ this.cleanupFns = [];
3252
+ this.pluginId = pluginId;
3253
+ this.stateManager = deps.stateManager;
3254
+ this.eventBus = deps.eventBus;
3255
+ this.container = deps.container;
3256
+ this.getPluginFn = deps.getPlugin;
3257
+ this.logger = {
3258
+ debug: (msg, metadata) => deps.logger.debug(`[${pluginId}] ${msg}`, metadata),
3259
+ info: (msg, metadata) => deps.logger.info(`[${pluginId}] ${msg}`, metadata),
3260
+ warn: (msg, metadata) => deps.logger.warn(`[${pluginId}] ${msg}`, metadata),
3261
+ error: (msg, metadata) => deps.logger.error(`[${pluginId}] ${msg}`, metadata)
3262
+ };
3263
+ }
3264
+ /**
3265
+ * Get a state value.
3266
+ *
3267
+ * @param key - State property key
3268
+ * @returns Current state value
3269
+ */
3270
+ getState(key) {
3271
+ return this.stateManager.getValue(key);
3272
+ }
3273
+ /**
3274
+ * Set a state value.
3275
+ *
3276
+ * @param key - State property key
3277
+ * @param value - New state value
3278
+ */
3279
+ setState(key, value) {
3280
+ this.stateManager.set(key, value);
3281
+ }
3282
+ /**
3283
+ * Subscribe to an event.
3284
+ *
3285
+ * @param event - Event name
3286
+ * @param handler - Event handler
3287
+ * @returns Unsubscribe function
3288
+ */
3289
+ on(event, handler) {
3290
+ return this.eventBus.on(event, handler);
3291
+ }
3292
+ /**
3293
+ * Unsubscribe from an event.
3294
+ *
3295
+ * @param event - Event name
3296
+ * @param handler - Event handler to remove
3297
+ */
3298
+ off(event, handler) {
3299
+ this.eventBus.off(event, handler);
3300
+ }
3301
+ /**
3302
+ * Emit an event.
3303
+ *
3304
+ * @param event - Event name
3305
+ * @param payload - Event payload
3306
+ */
3307
+ emit(event, payload) {
3308
+ this.eventBus.emit(event, payload);
3309
+ }
3310
+ /**
3311
+ * Get another plugin by ID (if ready).
3312
+ *
3313
+ * @param id - Plugin ID
3314
+ * @returns Plugin instance or null if not found/ready
3315
+ */
3316
+ getPlugin(id) {
3317
+ return this.getPluginFn(id);
3318
+ }
3319
+ /**
3320
+ * Register a cleanup function to run when plugin is destroyed.
3321
+ *
3322
+ * @param cleanup - Cleanup function
3323
+ */
3324
+ onDestroy(cleanup) {
3325
+ this.cleanupFns.push(cleanup);
3326
+ }
3327
+ /**
3328
+ * Subscribe to state changes.
3329
+ *
3330
+ * @param callback - Callback function called on any state change
3331
+ * @returns Unsubscribe function
3332
+ */
3333
+ subscribeToState(callback) {
3334
+ return this.stateManager.subscribe(callback);
3335
+ }
3336
+ /**
3337
+ * Run all registered cleanup functions.
3338
+ * Called by PluginManager when destroying the plugin.
3339
+ *
3340
+ * @internal
3341
+ */
3342
+ runCleanups() {
3343
+ for (const cleanup of this.cleanupFns) {
3344
+ try {
3345
+ cleanup();
3346
+ } catch (error) {
3347
+ this.logger.error("Cleanup function failed", { error });
3348
+ }
3349
+ }
3350
+ this.cleanupFns = [];
3351
+ }
3352
+ /**
3353
+ * Get all registered cleanup functions.
3354
+ *
3355
+ * @returns Array of cleanup functions
3356
+ * @internal
3357
+ */
3358
+ getCleanupFns() {
3359
+ return this.cleanupFns;
3360
+ }
3361
+ }
3362
+ class PluginManager {
3363
+ constructor(eventBus, stateManager, logger, options) {
3364
+ this.plugins = /* @__PURE__ */ new Map();
3365
+ this.eventBus = eventBus;
3366
+ this.stateManager = stateManager;
3367
+ this.logger = logger;
3368
+ this.container = options.container;
3369
+ }
3370
+ /** Register a plugin with optional configuration. */
3371
+ register(plugin, config) {
3372
+ if (this.plugins.has(plugin.id)) {
3373
+ throw new Error(`Plugin "${plugin.id}" is already registered`);
3374
+ }
3375
+ this.validatePlugin(plugin);
3376
+ const api = new PluginAPI(plugin.id, {
3377
+ stateManager: this.stateManager,
3378
+ eventBus: this.eventBus,
3379
+ logger: this.logger,
3380
+ container: this.container,
3381
+ getPlugin: (id) => this.getReadyPlugin(id)
3382
+ });
3383
+ this.plugins.set(plugin.id, {
3384
+ plugin,
3385
+ state: "registered",
3386
+ config,
3387
+ cleanupFns: [],
3388
+ api
3389
+ });
3390
+ this.logger.info(`Plugin registered: ${plugin.id}`);
3391
+ this.eventBus.emit("plugin:registered", { name: plugin.id, type: plugin.type });
3392
+ }
3393
+ /** Unregister a plugin. Destroys it first if active. */
3394
+ async unregister(id) {
3395
+ const record = this.plugins.get(id);
3396
+ if (!record) return;
3397
+ if (record.state === "ready") {
3398
+ await this.destroyPlugin(id);
3399
+ }
3400
+ this.plugins.delete(id);
3401
+ this.logger.info(`Plugin unregistered: ${id}`);
3402
+ }
3403
+ /** Initialize all registered plugins in dependency order. */
3404
+ async initAll() {
3405
+ const order = this.resolveDependencyOrder();
3406
+ for (const id of order) {
3407
+ await this.initPlugin(id);
3408
+ }
3409
+ }
3410
+ /** Initialize a specific plugin. */
3411
+ async initPlugin(id) {
3412
+ const record = this.plugins.get(id);
3413
+ if (!record) {
3414
+ throw new Error(`Plugin "${id}" not found`);
3415
+ }
3416
+ if (record.state === "ready") return;
3417
+ if (record.state === "initializing") {
3418
+ throw new Error(`Plugin "${id}" is already initializing (possible circular dependency)`);
3419
+ }
3420
+ for (const depId of record.plugin.dependencies || []) {
3421
+ const dep = this.plugins.get(depId);
3422
+ if (!dep) {
3423
+ throw new Error(`Plugin "${id}" depends on missing plugin "${depId}"`);
3424
+ }
3425
+ if (dep.state !== "ready") {
3426
+ await this.initPlugin(depId);
3427
+ }
3428
+ }
3429
+ try {
3430
+ record.state = "initializing";
3431
+ if (record.plugin.onStateChange) {
3432
+ const unsub = this.stateManager.subscribe(record.plugin.onStateChange.bind(record.plugin));
3433
+ record.api.onDestroy(unsub);
3434
+ }
3435
+ if (record.plugin.onError) {
3436
+ const unsub = this.eventBus.on("error", (err) => {
3437
+ record.plugin.onError?.(err.originalError || new Error(err.message));
3438
+ });
3439
+ record.api.onDestroy(unsub);
3440
+ }
3441
+ await record.plugin.init(record.api, record.config);
3442
+ record.state = "ready";
3443
+ this.logger.info(`Plugin ready: ${id}`);
3444
+ this.eventBus.emit("plugin:active", { name: id });
3445
+ } catch (error) {
3446
+ record.state = "error";
3447
+ record.error = error;
3448
+ this.logger.error(`Plugin init failed: ${id}`, { error });
3449
+ this.eventBus.emit("plugin:error", { name: id, error });
3450
+ throw error;
3451
+ }
3452
+ }
3453
+ /** Destroy all plugins in reverse dependency order. */
3454
+ async destroyAll() {
3455
+ const order = this.resolveDependencyOrder().reverse();
3456
+ for (const id of order) {
3457
+ await this.destroyPlugin(id);
3458
+ }
3459
+ }
3460
+ /** Destroy a specific plugin. */
3461
+ async destroyPlugin(id) {
3462
+ const record = this.plugins.get(id);
3463
+ if (!record || record.state !== "ready") return;
3464
+ try {
3465
+ await record.plugin.destroy();
3466
+ record.api.runCleanups();
3467
+ record.state = "registered";
3468
+ this.logger.info(`Plugin destroyed: ${id}`);
3469
+ this.eventBus.emit("plugin:destroyed", { name: id });
3470
+ } catch (error) {
3471
+ this.logger.error(`Plugin destroy failed: ${id}`, { error });
3472
+ record.state = "registered";
3473
+ }
3474
+ }
3475
+ /** Get a plugin by ID (returns any registered plugin). */
3476
+ getPlugin(id) {
3477
+ const record = this.plugins.get(id);
3478
+ return record ? record.plugin : null;
3479
+ }
3480
+ /** Get a plugin by ID only if ready (used by PluginAPI). */
3481
+ getReadyPlugin(id) {
3482
+ const record = this.plugins.get(id);
3483
+ return record?.state === "ready" ? record.plugin : null;
3484
+ }
3485
+ /** Check if a plugin is registered. */
3486
+ hasPlugin(id) {
3487
+ return this.plugins.has(id);
3488
+ }
3489
+ /** Get plugin state. */
3490
+ getPluginState(id) {
3491
+ return this.plugins.get(id)?.state ?? null;
3492
+ }
3493
+ /** Get all registered plugin IDs. */
3494
+ getPluginIds() {
3495
+ return Array.from(this.plugins.keys());
3496
+ }
3497
+ /** Get all ready plugins. */
3498
+ getReadyPlugins() {
3499
+ return Array.from(this.plugins.values()).filter((r) => r.state === "ready").map((r) => r.plugin);
3500
+ }
3501
+ /** Get plugins by type. */
3502
+ getPluginsByType(type) {
3503
+ return Array.from(this.plugins.values()).filter((r) => r.plugin.type === type).map((r) => r.plugin);
3504
+ }
3505
+ /** Select a provider plugin that can play a source. */
3506
+ selectProvider(source) {
3507
+ const providers = this.getPluginsByType("provider");
3508
+ for (const provider of providers) {
3509
+ const canPlay = provider.canPlay;
3510
+ if (typeof canPlay === "function" && canPlay(source)) {
3511
+ return provider;
3512
+ }
3513
+ }
3514
+ return null;
3515
+ }
3516
+ /** Resolve plugin initialization order using topological sort. */
3517
+ resolveDependencyOrder() {
3518
+ const visited = /* @__PURE__ */ new Set();
3519
+ const visiting = /* @__PURE__ */ new Set();
3520
+ const sorted = [];
3521
+ const visit = (id, path = []) => {
3522
+ if (visited.has(id)) return;
3523
+ if (visiting.has(id)) {
3524
+ const cycle = [...path, id].join(" -> ");
3525
+ throw new Error(`Circular dependency detected: ${cycle}`);
3526
+ }
3527
+ const record = this.plugins.get(id);
3528
+ if (!record) return;
3529
+ visiting.add(id);
3530
+ for (const depId of record.plugin.dependencies || []) {
3531
+ if (this.plugins.has(depId)) {
3532
+ visit(depId, [...path, id]);
3533
+ }
3534
+ }
3535
+ visiting.delete(id);
3536
+ visited.add(id);
3537
+ sorted.push(id);
3538
+ };
3539
+ for (const id of this.plugins.keys()) {
3540
+ visit(id);
3541
+ }
3542
+ return sorted;
3543
+ }
3544
+ /** Validate plugin has required properties. */
3545
+ validatePlugin(plugin) {
3546
+ if (!plugin.id || typeof plugin.id !== "string") {
3547
+ throw new Error("Plugin must have a valid id");
3548
+ }
3549
+ if (!plugin.name || typeof plugin.name !== "string") {
3550
+ throw new Error(`Plugin "${plugin.id}" must have a valid name`);
3551
+ }
3552
+ if (!plugin.version || typeof plugin.version !== "string") {
3553
+ throw new Error(`Plugin "${plugin.id}" must have a valid version`);
3554
+ }
3555
+ if (!plugin.type || typeof plugin.type !== "string") {
3556
+ throw new Error(`Plugin "${plugin.id}" must have a valid type`);
3557
+ }
3558
+ if (typeof plugin.init !== "function") {
3559
+ throw new Error(`Plugin "${plugin.id}" must have an init() method`);
3560
+ }
3561
+ if (typeof plugin.destroy !== "function") {
3562
+ throw new Error(`Plugin "${plugin.id}" must have a destroy() method`);
3563
+ }
3564
+ }
3565
+ }
3566
+ class ScarlettPlayer {
3567
+ /**
3568
+ * Create a new ScarlettPlayer.
3569
+ *
3570
+ * @param options - Player configuration
3571
+ */
3572
+ constructor(options) {
3573
+ this._currentProvider = null;
3574
+ this.destroyed = false;
3575
+ this.seekingWhilePlaying = false;
3576
+ this.seekResumeTimeout = null;
3577
+ if (typeof options.container === "string") {
3578
+ const el = document.querySelector(options.container);
3579
+ if (!el || !(el instanceof HTMLElement)) {
3580
+ throw new Error(`ScarlettPlayer: container not found: ${options.container}`);
3581
+ }
3582
+ this.container = el;
3583
+ } else if (options.container instanceof HTMLElement) {
3584
+ this.container = options.container;
3585
+ } else {
3586
+ throw new Error("ScarlettPlayer requires a valid HTMLElement container or CSS selector");
3587
+ }
3588
+ this.initialSrc = options.src;
3589
+ this.eventBus = new EventBus();
3590
+ this.stateManager = new StateManager({
3591
+ autoplay: options.autoplay ?? false,
3592
+ loop: options.loop ?? false,
3593
+ volume: options.volume ?? 1,
3594
+ muted: options.muted ?? false,
3595
+ poster: options.poster ?? ""
3596
+ });
3597
+ this.logger = new Logger({
3598
+ level: options.logLevel ?? "warn",
3599
+ scope: "ScarlettPlayer"
3600
+ });
3601
+ this.errorHandler = new ErrorHandler(this.eventBus, this.logger);
3602
+ this.pluginManager = new PluginManager(
3603
+ this.eventBus,
3604
+ this.stateManager,
3605
+ this.logger,
3606
+ { container: this.container }
3607
+ );
3608
+ if (options.plugins) {
3609
+ for (const plugin of options.plugins) {
3610
+ this.pluginManager.register(plugin);
3611
+ }
3612
+ }
3613
+ this.logger.info("ScarlettPlayer initialized", {
3614
+ autoplay: options.autoplay,
3615
+ plugins: options.plugins?.length ?? 0
3616
+ });
3617
+ this.eventBus.emit("player:ready", void 0);
3618
+ }
3619
+ /**
3620
+ * Initialize the player asynchronously.
3621
+ * Initializes non-provider plugins and loads initial source if provided.
3622
+ */
3623
+ async init() {
3624
+ this.checkDestroyed();
3625
+ for (const [id, record] of this.pluginManager.plugins) {
3626
+ if (record.plugin.type !== "provider" && record.state === "registered") {
3627
+ await this.pluginManager.initPlugin(id);
3628
+ }
3629
+ }
3630
+ if (this.initialSrc) {
3631
+ await this.load(this.initialSrc);
3632
+ }
3633
+ return Promise.resolve();
3634
+ }
3635
+ /**
3636
+ * Load a media source.
3637
+ *
3638
+ * Selects appropriate provider plugin and loads the source.
3639
+ *
3640
+ * @param source - Media source URL
3641
+ * @returns Promise that resolves when source is loaded
3642
+ *
3643
+ * @example
3644
+ * ```ts
3645
+ * await player.load('video.m3u8');
3646
+ * ```
3647
+ */
3648
+ async load(source) {
3649
+ this.checkDestroyed();
3650
+ try {
3651
+ this.logger.info("Loading source", { source });
3652
+ this.stateManager.update({
3653
+ playing: false,
3654
+ paused: true,
3655
+ ended: false,
3656
+ buffering: true,
3657
+ currentTime: 0,
3658
+ duration: 0,
3659
+ bufferedAmount: 0,
3660
+ playbackState: "loading"
3661
+ });
3662
+ if (this._currentProvider) {
3663
+ const previousProviderId = this._currentProvider.id;
3664
+ this.logger.info("Destroying previous provider", { provider: previousProviderId });
3665
+ await this.pluginManager.destroyPlugin(previousProviderId);
3666
+ this._currentProvider = null;
3667
+ }
3668
+ const provider = this.pluginManager.selectProvider(source);
3669
+ if (!provider) {
3670
+ this.errorHandler.throw(
3671
+ ErrorCode.PROVIDER_NOT_FOUND,
3672
+ `No provider found for source: ${source}`,
3673
+ {
3674
+ fatal: true,
3675
+ context: { source }
3676
+ }
3677
+ );
3678
+ return;
3679
+ }
3680
+ this._currentProvider = provider;
3681
+ this.logger.info("Provider selected", { provider: provider.id });
3682
+ await this.pluginManager.initPlugin(provider.id);
3683
+ this.stateManager.set("source", { src: source, type: this.detectMimeType(source) });
3684
+ if (typeof provider.loadSource === "function") {
3685
+ await provider.loadSource(source);
3686
+ }
3687
+ if (this.stateManager.getValue("autoplay")) {
3688
+ await this.play();
3689
+ }
3690
+ } catch (error) {
3691
+ this.errorHandler.handle(error, {
3692
+ operation: "load",
3693
+ source
3694
+ });
3695
+ }
3696
+ }
3697
+ /**
3698
+ * Start playback.
3699
+ *
3700
+ * @returns Promise that resolves when playback starts
3701
+ *
3702
+ * @example
3703
+ * ```ts
3704
+ * await player.play();
3705
+ * ```
3706
+ */
3707
+ async play() {
3708
+ this.checkDestroyed();
3709
+ try {
3710
+ this.logger.debug("Play requested");
3711
+ this.eventBus.emit("playback:play", void 0);
3712
+ } catch (error) {
3713
+ this.errorHandler.handle(error, { operation: "play" });
3714
+ }
3715
+ }
3716
+ /**
3717
+ * Pause playback.
3718
+ *
3719
+ * @example
3720
+ * ```ts
3721
+ * player.pause();
3722
+ * ```
3723
+ */
3724
+ pause() {
3725
+ this.checkDestroyed();
3726
+ try {
3727
+ this.logger.debug("Pause requested");
3728
+ this.seekingWhilePlaying = false;
3729
+ if (this.seekResumeTimeout !== null) {
3730
+ clearTimeout(this.seekResumeTimeout);
3731
+ this.seekResumeTimeout = null;
3732
+ }
3733
+ this.eventBus.emit("playback:pause", void 0);
3734
+ } catch (error) {
3735
+ this.errorHandler.handle(error, { operation: "pause" });
3736
+ }
3737
+ }
3738
+ /**
3739
+ * Seek to a specific time.
3740
+ *
3741
+ * @param time - Time in seconds
3742
+ *
3743
+ * @example
3744
+ * ```ts
3745
+ * player.seek(30); // Seek to 30 seconds
3746
+ * ```
3747
+ */
3748
+ seek(time) {
3749
+ this.checkDestroyed();
3750
+ try {
3751
+ this.logger.debug("Seek requested", { time });
3752
+ const wasPlaying = this.stateManager.getValue("playing");
3753
+ if (wasPlaying) {
3754
+ this.seekingWhilePlaying = true;
3755
+ }
3756
+ if (this.seekResumeTimeout !== null) {
3757
+ clearTimeout(this.seekResumeTimeout);
3758
+ this.seekResumeTimeout = null;
3759
+ }
3760
+ this.eventBus.emit("playback:seeking", { time });
3761
+ this.stateManager.set("currentTime", time);
3762
+ if (this.seekingWhilePlaying) {
3763
+ this.seekResumeTimeout = setTimeout(() => {
3764
+ if (this.seekingWhilePlaying && this.stateManager.getValue("playing")) {
3765
+ this.logger.debug("Resuming playback after seek");
3766
+ this.seekingWhilePlaying = false;
3767
+ this.eventBus.emit("playback:play", void 0);
3768
+ }
3769
+ this.seekResumeTimeout = null;
3770
+ }, 300);
3771
+ }
3772
+ } catch (error) {
3773
+ this.errorHandler.handle(error, { operation: "seek", time });
3774
+ }
3775
+ }
3776
+ /**
3777
+ * Set volume.
3778
+ *
3779
+ * @param volume - Volume 0-1
3780
+ *
3781
+ * @example
3782
+ * ```ts
3783
+ * player.setVolume(0.5); // 50% volume
3784
+ * ```
3785
+ */
3786
+ setVolume(volume) {
3787
+ this.checkDestroyed();
3788
+ const clampedVolume = Math.max(0, Math.min(1, volume));
3789
+ this.stateManager.set("volume", clampedVolume);
3790
+ this.eventBus.emit("volume:change", {
3791
+ volume: clampedVolume,
3792
+ muted: this.stateManager.getValue("muted")
3793
+ });
3794
+ }
3795
+ /**
3796
+ * Set muted state.
3797
+ *
3798
+ * @param muted - Mute flag
3799
+ *
3800
+ * @example
3801
+ * ```ts
3802
+ * player.setMuted(true);
3803
+ * ```
3804
+ */
3805
+ setMuted(muted) {
3806
+ this.checkDestroyed();
3807
+ this.stateManager.set("muted", muted);
3808
+ this.eventBus.emit("volume:mute", { muted });
3809
+ }
3810
+ /**
3811
+ * Set playback rate.
3812
+ *
3813
+ * @param rate - Playback rate (e.g., 1.0 = normal, 2.0 = 2x speed)
3814
+ *
3815
+ * @example
3816
+ * ```ts
3817
+ * player.setPlaybackRate(1.5); // 1.5x speed
3818
+ * ```
3819
+ */
3820
+ setPlaybackRate(rate) {
3821
+ this.checkDestroyed();
3822
+ this.stateManager.set("playbackRate", rate);
3823
+ this.eventBus.emit("playback:ratechange", { rate });
3824
+ }
3825
+ /**
3826
+ * Set autoplay state.
3827
+ *
3828
+ * When enabled, videos will automatically play after loading.
3829
+ *
3830
+ * @param autoplay - Autoplay flag
3831
+ *
3832
+ * @example
3833
+ * ```ts
3834
+ * player.setAutoplay(true);
3835
+ * await player.load('video.mp4'); // Will auto-play
3836
+ * ```
3837
+ */
3838
+ setAutoplay(autoplay) {
3839
+ this.checkDestroyed();
3840
+ this.stateManager.set("autoplay", autoplay);
3841
+ this.logger.debug("Autoplay set", { autoplay });
3842
+ }
3843
+ /**
3844
+ * Subscribe to an event.
3845
+ *
3846
+ * @param event - Event name
3847
+ * @param handler - Event handler
3848
+ * @returns Unsubscribe function
3849
+ *
3850
+ * @example
3851
+ * ```ts
3852
+ * const unsub = player.on('playback:play', () => {
3853
+ * console.log('Playing!');
3854
+ * });
3855
+ *
3856
+ * // Later: unsubscribe
3857
+ * unsub();
3858
+ * ```
3859
+ */
3860
+ on(event, handler) {
3861
+ this.checkDestroyed();
3862
+ return this.eventBus.on(event, handler);
3863
+ }
3864
+ /**
3865
+ * Subscribe to an event once.
3866
+ *
3867
+ * @param event - Event name
3868
+ * @param handler - Event handler
3869
+ * @returns Unsubscribe function
3870
+ *
3871
+ * @example
3872
+ * ```ts
3873
+ * player.once('player:ready', () => {
3874
+ * console.log('Player ready!');
3875
+ * });
3876
+ * ```
3877
+ */
3878
+ once(event, handler) {
3879
+ this.checkDestroyed();
3880
+ return this.eventBus.once(event, handler);
3881
+ }
3882
+ /**
3883
+ * Get a plugin by name.
3884
+ *
3885
+ * @param name - Plugin name
3886
+ * @returns Plugin instance or null
3887
+ *
3888
+ * @example
3889
+ * ```ts
3890
+ * const hls = player.getPlugin('hls-plugin');
3891
+ * ```
3892
+ */
3893
+ getPlugin(name) {
3894
+ this.checkDestroyed();
3895
+ return this.pluginManager.getPlugin(name);
3896
+ }
3897
+ /**
3898
+ * Register a plugin.
3899
+ *
3900
+ * @param plugin - Plugin to register
3901
+ *
3902
+ * @example
3903
+ * ```ts
3904
+ * player.registerPlugin(myPlugin);
3905
+ * ```
3906
+ */
3907
+ registerPlugin(plugin) {
3908
+ this.checkDestroyed();
3909
+ this.pluginManager.register(plugin);
3910
+ }
3911
+ /**
3912
+ * Get current state snapshot.
3913
+ *
3914
+ * @returns Readonly state snapshot
3915
+ *
3916
+ * @example
3917
+ * ```ts
3918
+ * const state = player.getState();
3919
+ * console.log(state.playing, state.currentTime);
3920
+ * ```
3921
+ */
3922
+ getState() {
3923
+ this.checkDestroyed();
3924
+ return this.stateManager.snapshot();
3925
+ }
3926
+ // ===== Quality Methods (proxied to provider) =====
3927
+ /**
3928
+ * Get available quality levels from the current provider.
3929
+ * @returns Array of quality levels or empty array if not available
3930
+ */
3931
+ getQualities() {
3932
+ this.checkDestroyed();
3933
+ if (!this._currentProvider) return [];
3934
+ const provider = this._currentProvider;
3935
+ if (typeof provider.getLevels === "function") {
3936
+ return provider.getLevels();
3937
+ }
3938
+ return [];
3939
+ }
3940
+ /**
3941
+ * Set quality level (-1 for auto).
3942
+ * @param index - Quality level index
3943
+ */
3944
+ setQuality(index) {
3945
+ this.checkDestroyed();
3946
+ if (!this._currentProvider) {
3947
+ this.logger.warn("No provider available for quality change");
3948
+ return;
3949
+ }
3950
+ const provider = this._currentProvider;
3951
+ if (typeof provider.setLevel === "function") {
3952
+ provider.setLevel(index);
3953
+ this.eventBus.emit("quality:change", {
3954
+ quality: index === -1 ? "auto" : `level-${index}`,
3955
+ auto: index === -1
3956
+ });
3957
+ }
3958
+ }
3959
+ /**
3960
+ * Get current quality level index (-1 = auto).
3961
+ */
3962
+ getCurrentQuality() {
3963
+ this.checkDestroyed();
3964
+ if (!this._currentProvider) return -1;
3965
+ const provider = this._currentProvider;
3966
+ if (typeof provider.getCurrentLevel === "function") {
3967
+ return provider.getCurrentLevel();
3968
+ }
3969
+ return -1;
3970
+ }
3971
+ // ===== Fullscreen Methods =====
3972
+ /**
3973
+ * Request fullscreen mode.
3974
+ */
3975
+ async requestFullscreen() {
3976
+ this.checkDestroyed();
3977
+ try {
3978
+ if (this.container.requestFullscreen) {
3979
+ await this.container.requestFullscreen();
3980
+ } else if (this.container.webkitRequestFullscreen) {
3981
+ await this.container.webkitRequestFullscreen();
3982
+ }
3983
+ this.stateManager.set("fullscreen", true);
3984
+ this.eventBus.emit("fullscreen:change", { fullscreen: true });
3985
+ } catch (error) {
3986
+ this.logger.error("Fullscreen request failed", { error });
3987
+ }
3988
+ }
3989
+ /**
3990
+ * Exit fullscreen mode.
3991
+ */
3992
+ async exitFullscreen() {
3993
+ this.checkDestroyed();
3994
+ try {
3995
+ if (document.exitFullscreen) {
3996
+ await document.exitFullscreen();
3997
+ } else if (document.webkitExitFullscreen) {
3998
+ await document.webkitExitFullscreen();
3999
+ }
4000
+ this.stateManager.set("fullscreen", false);
4001
+ this.eventBus.emit("fullscreen:change", { fullscreen: false });
4002
+ } catch (error) {
4003
+ this.logger.error("Exit fullscreen failed", { error });
4004
+ }
4005
+ }
4006
+ /**
4007
+ * Toggle fullscreen mode.
4008
+ */
4009
+ async toggleFullscreen() {
4010
+ if (this.fullscreen) {
4011
+ await this.exitFullscreen();
4012
+ } else {
4013
+ await this.requestFullscreen();
4014
+ }
4015
+ }
4016
+ // ===== Casting Methods (proxied to plugins) =====
4017
+ /**
4018
+ * Request AirPlay (proxied to airplay plugin).
4019
+ */
4020
+ requestAirPlay() {
4021
+ this.checkDestroyed();
4022
+ const airplay = this.pluginManager.getPlugin("airplay");
4023
+ if (airplay && typeof airplay.showPicker === "function") {
4024
+ airplay.showPicker();
4025
+ } else {
4026
+ this.logger.warn("AirPlay plugin not available");
4027
+ }
4028
+ }
4029
+ /**
4030
+ * Request Chromecast session (proxied to chromecast plugin).
4031
+ */
4032
+ async requestChromecast() {
4033
+ this.checkDestroyed();
4034
+ const chromecast = this.pluginManager.getPlugin("chromecast");
4035
+ if (chromecast && typeof chromecast.requestSession === "function") {
4036
+ await chromecast.requestSession();
4037
+ } else {
4038
+ this.logger.warn("Chromecast plugin not available");
4039
+ }
4040
+ }
4041
+ /**
4042
+ * Stop casting (AirPlay or Chromecast).
4043
+ */
4044
+ stopCasting() {
4045
+ this.checkDestroyed();
4046
+ const airplay = this.pluginManager.getPlugin("airplay");
4047
+ if (airplay && typeof airplay.stop === "function") {
4048
+ airplay.stop();
4049
+ }
4050
+ const chromecast = this.pluginManager.getPlugin("chromecast");
4051
+ if (chromecast && typeof chromecast.stopSession === "function") {
4052
+ chromecast.stopSession();
4053
+ }
4054
+ }
4055
+ // ===== Live Stream Methods =====
4056
+ /**
4057
+ * Seek to live edge (for live streams).
4058
+ */
4059
+ seekToLive() {
4060
+ this.checkDestroyed();
4061
+ const isLive = this.stateManager.getValue("live");
4062
+ if (!isLive) {
4063
+ this.logger.warn("Not a live stream");
4064
+ return;
4065
+ }
4066
+ if (this._currentProvider) {
4067
+ const provider = this._currentProvider;
4068
+ if (typeof provider.getLiveInfo === "function") {
4069
+ const liveInfo = provider.getLiveInfo();
4070
+ if (liveInfo?.liveSyncPosition !== void 0) {
4071
+ this.seek(liveInfo.liveSyncPosition);
4072
+ return;
4073
+ }
4074
+ }
4075
+ }
4076
+ const duration = this.stateManager.getValue("duration");
4077
+ if (duration > 0) {
4078
+ this.seek(duration);
4079
+ }
4080
+ }
4081
+ /**
4082
+ * Destroy the player and cleanup all resources.
4083
+ *
4084
+ * @example
4085
+ * ```ts
4086
+ * player.destroy();
4087
+ * ```
4088
+ */
4089
+ destroy() {
4090
+ if (this.destroyed) {
4091
+ return;
4092
+ }
4093
+ this.logger.info("Destroying player");
4094
+ if (this.seekResumeTimeout !== null) {
4095
+ clearTimeout(this.seekResumeTimeout);
4096
+ this.seekResumeTimeout = null;
4097
+ }
4098
+ this.eventBus.emit("player:destroy", void 0);
4099
+ this.pluginManager.destroyAll();
4100
+ this.eventBus.destroy();
4101
+ this.stateManager.destroy();
4102
+ this.destroyed = true;
4103
+ this.logger.info("Player destroyed");
4104
+ }
4105
+ // ===== State Getters =====
4106
+ /**
4107
+ * Get playing state.
4108
+ */
4109
+ get playing() {
4110
+ return this.stateManager.getValue("playing");
4111
+ }
4112
+ /**
4113
+ * Get paused state.
4114
+ */
4115
+ get paused() {
4116
+ return this.stateManager.getValue("paused");
4117
+ }
4118
+ /**
4119
+ * Get current time in seconds.
4120
+ */
4121
+ get currentTime() {
4122
+ return this.stateManager.getValue("currentTime");
4123
+ }
4124
+ /**
4125
+ * Get duration in seconds.
4126
+ */
4127
+ get duration() {
4128
+ return this.stateManager.getValue("duration");
4129
+ }
4130
+ /**
4131
+ * Get volume (0-1).
4132
+ */
4133
+ get volume() {
4134
+ return this.stateManager.getValue("volume");
4135
+ }
4136
+ /**
4137
+ * Get muted state.
4138
+ */
4139
+ get muted() {
4140
+ return this.stateManager.getValue("muted");
4141
+ }
4142
+ /**
4143
+ * Get playback rate.
4144
+ */
4145
+ get playbackRate() {
4146
+ return this.stateManager.getValue("playbackRate");
4147
+ }
4148
+ /**
4149
+ * Get buffered amount (0-1).
4150
+ */
4151
+ get bufferedAmount() {
4152
+ return this.stateManager.getValue("bufferedAmount");
4153
+ }
4154
+ /**
4155
+ * Get current provider plugin.
4156
+ */
4157
+ get currentProvider() {
4158
+ return this._currentProvider;
4159
+ }
4160
+ /**
4161
+ * Get fullscreen state.
4162
+ */
4163
+ get fullscreen() {
4164
+ return this.stateManager.getValue("fullscreen");
4165
+ }
4166
+ /**
4167
+ * Get live stream state.
4168
+ */
4169
+ get live() {
4170
+ return this.stateManager.getValue("live");
4171
+ }
4172
+ /**
4173
+ * Get autoplay state.
4174
+ */
4175
+ get autoplay() {
4176
+ return this.stateManager.getValue("autoplay");
4177
+ }
4178
+ /**
4179
+ * Check if player is destroyed.
4180
+ * @private
4181
+ */
4182
+ checkDestroyed() {
4183
+ if (this.destroyed) {
4184
+ throw new Error("Cannot call methods on destroyed player");
4185
+ }
4186
+ }
4187
+ /**
4188
+ * Detect MIME type from source URL.
4189
+ * @private
4190
+ */
4191
+ detectMimeType(source) {
4192
+ const ext = source.split(".").pop()?.toLowerCase();
4193
+ switch (ext) {
4194
+ case "m3u8":
4195
+ return "application/x-mpegURL";
4196
+ case "mpd":
4197
+ return "application/dash+xml";
4198
+ case "mp4":
4199
+ return "video/mp4";
4200
+ case "webm":
4201
+ return "video/webm";
4202
+ case "ogg":
4203
+ return "video/ogg";
4204
+ default:
4205
+ return "video/mp4";
4206
+ }
4207
+ }
4208
+ }
4209
+ async function createPlayer(options) {
4210
+ const player = new ScarlettPlayer(options);
4211
+ await player.init();
4212
+ return player;
4213
+ }
4214
+ function getAttr(element, ...names) {
4215
+ for (const name of names) {
4216
+ const value = element.getAttribute(name);
4217
+ if (value !== null) return value;
4218
+ }
4219
+ return null;
4220
+ }
4221
+ function parseDataAttributes(element) {
4222
+ const config = {};
4223
+ const src = getAttr(element, "data-src", "src", "href");
4224
+ if (src) {
4225
+ config.src = src;
4226
+ }
4227
+ const type = getAttr(element, "data-type", "type");
4228
+ if (type && ["video", "audio", "audio-mini"].includes(type)) {
4229
+ config.type = type;
4230
+ }
4231
+ const autoplay = getAttr(element, "data-autoplay", "autoplay");
4232
+ if (autoplay !== null) {
4233
+ config.autoplay = autoplay !== "false";
4234
+ }
4235
+ const muted = getAttr(element, "data-muted", "muted");
4236
+ if (muted !== null) {
4237
+ config.muted = muted !== "false";
4238
+ }
4239
+ const controls = getAttr(element, "data-controls", "controls");
4240
+ if (controls !== null) {
4241
+ config.controls = controls !== "false";
4242
+ }
4243
+ const keyboard = getAttr(element, "data-keyboard", "keyboard");
4244
+ if (keyboard !== null) {
4245
+ config.keyboard = keyboard !== "false";
4246
+ }
4247
+ const loop = getAttr(element, "data-loop", "loop");
4248
+ if (loop !== null) {
4249
+ config.loop = loop !== "false";
4250
+ }
4251
+ const poster = getAttr(element, "data-poster", "poster");
4252
+ if (poster) {
4253
+ config.poster = poster;
4254
+ }
4255
+ const artwork = getAttr(element, "data-artwork", "artwork");
4256
+ if (artwork) {
4257
+ config.artwork = artwork;
4258
+ }
4259
+ const title = getAttr(element, "data-title", "title");
4260
+ if (title) {
4261
+ config.title = title;
4262
+ }
4263
+ const artist = getAttr(element, "data-artist", "artist");
4264
+ if (artist) {
4265
+ config.artist = artist;
4266
+ }
4267
+ const album = getAttr(element, "data-album", "album");
4268
+ if (album) {
4269
+ config.album = album;
4270
+ }
4271
+ const brandColor = getAttr(element, "data-brand-color", "data-color", "color");
4272
+ if (brandColor) {
4273
+ config.brandColor = brandColor;
4274
+ }
4275
+ const primaryColor = element.getAttribute("data-primary-color");
4276
+ if (primaryColor) {
4277
+ config.primaryColor = primaryColor;
4278
+ }
4279
+ const backgroundColor = element.getAttribute("data-background-color");
4280
+ if (backgroundColor) {
4281
+ config.backgroundColor = backgroundColor;
4282
+ }
4283
+ const width = element.getAttribute("data-width");
4284
+ if (width) {
4285
+ config.width = width;
4286
+ }
4287
+ const height = element.getAttribute("data-height");
4288
+ if (height) {
4289
+ config.height = height;
4290
+ }
4291
+ const aspectRatio = element.getAttribute("data-aspect-ratio");
4292
+ if (aspectRatio) {
4293
+ config.aspectRatio = aspectRatio;
4294
+ }
4295
+ const className = element.getAttribute("data-class");
4296
+ if (className) {
4297
+ config.className = className;
4298
+ }
4299
+ const hideDelay = element.getAttribute("data-hide-delay");
4300
+ if (hideDelay) {
4301
+ const parsed = parseInt(hideDelay, 10);
4302
+ if (!isNaN(parsed)) {
4303
+ config.hideDelay = parsed;
4304
+ }
4305
+ }
4306
+ const playbackRate = element.getAttribute("data-playback-rate");
4307
+ if (playbackRate) {
4308
+ const parsed = parseFloat(playbackRate);
4309
+ if (!isNaN(parsed)) {
4310
+ config.playbackRate = parsed;
4311
+ }
4312
+ }
4313
+ const startTime = element.getAttribute("data-start-time");
4314
+ if (startTime) {
4315
+ const parsed = parseFloat(startTime);
4316
+ if (!isNaN(parsed)) {
4317
+ config.startTime = parsed;
4318
+ }
4319
+ }
4320
+ const playlist = element.getAttribute("data-playlist");
4321
+ if (playlist) {
4322
+ try {
4323
+ config.playlist = JSON.parse(playlist);
4324
+ } catch {
4325
+ console.warn("[ScarlettPlayer] Invalid playlist JSON");
4326
+ }
4327
+ }
4328
+ const analyticsBeaconUrl = element.getAttribute("data-analytics-beacon-url");
4329
+ if (analyticsBeaconUrl) {
4330
+ config.analytics = {
4331
+ beaconUrl: analyticsBeaconUrl,
4332
+ apiKey: element.getAttribute("data-analytics-api-key") || void 0,
4333
+ videoId: element.getAttribute("data-analytics-video-id") || void 0
4334
+ };
4335
+ }
4336
+ return config;
4337
+ }
4338
+ function aspectRatioToPercent(ratio) {
4339
+ const parts = ratio.split(":").map(Number);
4340
+ const width = parts[0];
4341
+ const height = parts[1];
4342
+ if (parts.length === 2 && width !== void 0 && height !== void 0 && !isNaN(width) && !isNaN(height) && width > 0) {
4343
+ return height / width * 100;
4344
+ }
4345
+ return 56.25;
4346
+ }
4347
+ function applyContainerStyles(container, config) {
4348
+ const type = config.type || "video";
4349
+ if (config.className) {
4350
+ container.classList.add(...config.className.split(" "));
4351
+ }
4352
+ if (config.width) {
4353
+ container.style.width = config.width;
4354
+ }
4355
+ if (type === "video") {
4356
+ if (config.height) {
4357
+ container.style.height = config.height;
4358
+ } else if (config.aspectRatio) {
4359
+ container.style.position = "relative";
4360
+ container.style.paddingBottom = `${aspectRatioToPercent(config.aspectRatio)}%`;
4361
+ container.style.height = "0";
4362
+ }
4363
+ } else if (type === "audio") {
4364
+ container.style.position = container.style.position || "relative";
4365
+ container.style.height = config.height || "120px";
4366
+ container.style.width = container.style.width || "100%";
4367
+ } else if (type === "audio-mini") {
4368
+ container.style.position = container.style.position || "relative";
4369
+ container.style.height = config.height || "64px";
4370
+ container.style.width = container.style.width || "100%";
4371
+ }
4372
+ }
4373
+ async function createEmbedPlayer(container, config, pluginCreators2, availableTypes) {
4374
+ const type = config.type || "video";
4375
+ if (!availableTypes.includes(type)) {
4376
+ const buildSuggestion = type === "video" ? "Use embed.js or embed.video.js" : "Use embed.js or embed.audio.js";
4377
+ throw new Error(
4378
+ `[ScarlettPlayer] Player type "${type}" is not available in this build. ${buildSuggestion}`
4379
+ );
4380
+ }
4381
+ if (!config.src && !config.playlist?.length) {
4382
+ console.error("[ScarlettPlayer] No source URL or playlist provided");
4383
+ return null;
4384
+ }
4385
+ try {
4386
+ applyContainerStyles(container, config);
4387
+ const theme = {};
4388
+ if (config.brandColor) theme.accentColor = config.brandColor;
4389
+ if (config.primaryColor) theme.primaryColor = config.primaryColor;
4390
+ if (config.backgroundColor) theme.backgroundColor = config.backgroundColor;
4391
+ const plugins = [pluginCreators2.hls()];
4392
+ if (pluginCreators2.playlist && config.playlist?.length) {
4393
+ plugins.push(pluginCreators2.playlist({
4394
+ items: config.playlist.map((item, index) => ({
4395
+ id: `item-${index}`,
4396
+ src: item.src,
4397
+ title: item.title,
4398
+ artist: item.artist,
4399
+ poster: item.poster || item.artwork,
4400
+ duration: item.duration
4401
+ }))
4402
+ }));
4403
+ }
4404
+ if (pluginCreators2.mediaSession && (type !== "video" || config.title)) {
4405
+ plugins.push(pluginCreators2.mediaSession({
4406
+ title: config.title || config.playlist?.[0]?.title,
4407
+ artist: config.artist || config.playlist?.[0]?.artist,
4408
+ album: config.album,
4409
+ artwork: config.artwork || config.poster || config.playlist?.[0]?.artwork
4410
+ }));
4411
+ }
4412
+ if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
4413
+ plugins.push(pluginCreators2.analytics({
4414
+ beaconUrl: config.analytics.beaconUrl,
4415
+ apiKey: config.analytics.apiKey,
4416
+ videoId: config.analytics.videoId || config.src || "unknown"
4417
+ }));
4418
+ }
4419
+ if (config.controls !== false) {
4420
+ if (type === "video" && pluginCreators2.videoUI) {
4421
+ const uiConfig = {};
4422
+ if (Object.keys(theme).length > 0) uiConfig.theme = theme;
4423
+ if (config.hideDelay !== void 0) uiConfig.hideDelay = config.hideDelay;
4424
+ plugins.push(pluginCreators2.videoUI(uiConfig));
4425
+ } else if ((type === "audio" || type === "audio-mini") && pluginCreators2.audioUI) {
4426
+ plugins.push(pluginCreators2.audioUI({
4427
+ layout: type === "audio-mini" ? "compact" : "full",
4428
+ theme: {
4429
+ primary: config.brandColor,
4430
+ text: config.primaryColor,
4431
+ background: config.backgroundColor
4432
+ }
4433
+ }));
4434
+ }
4435
+ }
4436
+ const player = await createPlayer({
4437
+ container,
4438
+ src: config.src || config.playlist?.[0]?.src || "",
4439
+ autoplay: config.autoplay || false,
4440
+ muted: config.muted || false,
4441
+ poster: config.poster || config.artwork || config.playlist?.[0]?.poster,
4442
+ loop: config.loop || false,
4443
+ plugins
4444
+ });
4445
+ const video = container.querySelector("video");
4446
+ if (video) {
4447
+ if (config.playbackRate) video.playbackRate = config.playbackRate;
4448
+ if (config.startTime) video.currentTime = config.startTime;
4449
+ }
4450
+ return player;
4451
+ } catch (error) {
4452
+ console.error("[ScarlettPlayer] Failed to create player:", error);
4453
+ throw error;
4454
+ }
4455
+ }
4456
+ async function initElement(element, pluginCreators2, availableTypes) {
4457
+ if (element.hasAttribute("data-scarlett-initialized")) {
4458
+ return null;
4459
+ }
4460
+ const config = parseDataAttributes(element);
4461
+ element.setAttribute("data-scarlett-initialized", "true");
4462
+ try {
4463
+ const player = await createEmbedPlayer(element, config, pluginCreators2, availableTypes);
4464
+ return player;
4465
+ } catch (error) {
4466
+ element.removeAttribute("data-scarlett-initialized");
4467
+ throw error;
4468
+ }
4469
+ }
4470
+ const PLAYER_SELECTORS = [
4471
+ "[data-scarlett-player]",
4472
+ "[data-sp]",
4473
+ ".scarlett-player"
4474
+ ];
4475
+ async function initAll(pluginCreators2, availableTypes) {
4476
+ const selector = PLAYER_SELECTORS.join(", ");
4477
+ const elements = document.querySelectorAll(selector);
4478
+ let initialized = 0;
4479
+ let errors = 0;
4480
+ for (const element of Array.from(elements)) {
4481
+ try {
4482
+ const player = await initElement(element, pluginCreators2, availableTypes);
4483
+ if (player) initialized++;
4484
+ } catch (error) {
4485
+ errors++;
4486
+ console.error("[ScarlettPlayer] Failed to initialize element:", error);
4487
+ }
4488
+ }
4489
+ if (initialized > 0) {
4490
+ console.log(`[ScarlettPlayer] Initialized ${initialized} player(s)`);
4491
+ }
4492
+ if (errors > 0) {
4493
+ console.warn(`[ScarlettPlayer] ${errors} player(s) failed to initialize`);
4494
+ }
4495
+ }
4496
+ function createScarlettPlayerAPI(pluginCreators2, availableTypes, version) {
4497
+ return {
4498
+ version,
4499
+ availableTypes,
4500
+ async create(options) {
4501
+ let container = null;
4502
+ if (typeof options.container === "string") {
4503
+ container = document.querySelector(options.container);
4504
+ if (!container) {
4505
+ console.error(`[ScarlettPlayer] Container not found: ${options.container}`);
4506
+ return null;
4507
+ }
4508
+ } else {
4509
+ container = options.container;
4510
+ }
4511
+ return createEmbedPlayer(container, options, pluginCreators2, availableTypes);
4512
+ },
4513
+ async initAll() {
4514
+ return initAll(pluginCreators2, availableTypes);
4515
+ }
4516
+ };
4517
+ }
4518
+ function setupAutoInit(pluginCreators2, availableTypes) {
4519
+ if (typeof document !== "undefined") {
4520
+ if (document.readyState === "loading") {
4521
+ document.addEventListener("DOMContentLoaded", () => {
4522
+ initAll(pluginCreators2, availableTypes);
4523
+ });
4524
+ } else {
4525
+ initAll(pluginCreators2, availableTypes);
4526
+ }
4527
+ }
4528
+ }
4529
+ const VERSION = "0.3.0-audio";
4530
+ const AVAILABLE_TYPES = ["audio", "audio-mini"];
4531
+ const pluginCreators = {
4532
+ hls: createHLSPlugin,
4533
+ audioUI: createAudioUIPlugin,
4534
+ playlist: createPlaylistPlugin,
4535
+ mediaSession: createMediaSessionPlugin
4536
+ // Video UI not available in this build
4537
+ // Analytics not available in this build
4538
+ };
4539
+ const ScarlettPlayerAPI = createScarlettPlayerAPI(
4540
+ pluginCreators,
4541
+ AVAILABLE_TYPES,
4542
+ VERSION
4543
+ );
4544
+ if (typeof window !== "undefined") {
4545
+ window.ScarlettPlayer = ScarlettPlayerAPI;
4546
+ }
4547
+ setupAutoInit(pluginCreators, AVAILABLE_TYPES);
4548
+ export {
4549
+ applyContainerStyles,
4550
+ aspectRatioToPercent,
4551
+ ScarlettPlayerAPI as default,
4552
+ parseDataAttributes
4553
+ };
4554
+ //# sourceMappingURL=embed.audio.js.map