@scarlett-player/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/error-handler.d.ts.map +1 -0
  2. package/dist/error-handler.js +300 -0
  3. package/dist/error-handler.js.map +1 -0
  4. package/dist/events/event-bus.d.ts.map +1 -0
  5. package/dist/events/event-bus.js +407 -0
  6. package/dist/events/event-bus.js.map +1 -0
  7. package/dist/index.cjs +2 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +2271 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/logger.d.ts.map +1 -0
  14. package/dist/logger.js +272 -0
  15. package/dist/logger.js.map +1 -0
  16. package/dist/plugin-api.d.ts +147 -0
  17. package/dist/plugin-api.d.ts.map +1 -0
  18. package/dist/plugin-api.js +160 -0
  19. package/dist/plugin-api.js.map +1 -0
  20. package/dist/plugin-manager.d.ts +52 -0
  21. package/dist/plugin-manager.d.ts.map +1 -0
  22. package/dist/plugin-manager.js +224 -0
  23. package/dist/plugin-manager.js.map +1 -0
  24. package/dist/scarlett-player.d.ts +404 -0
  25. package/dist/scarlett-player.d.ts.map +1 -0
  26. package/dist/scarlett-player.js +769 -0
  27. package/dist/scarlett-player.js.map +1 -0
  28. package/dist/state/computed.d.ts.map +1 -0
  29. package/dist/state/computed.js +134 -0
  30. package/dist/state/computed.js.map +1 -0
  31. package/dist/state/effect.d.ts.map +1 -0
  32. package/dist/state/effect.js +77 -0
  33. package/dist/state/effect.js.map +1 -0
  34. package/dist/state/index.d.ts.map +1 -0
  35. package/dist/state/index.js +9 -0
  36. package/dist/state/index.js.map +1 -0
  37. package/dist/state/signal.d.ts.map +1 -0
  38. package/dist/state/signal.js +126 -0
  39. package/dist/state/signal.js.map +1 -0
  40. package/dist/state/state-manager.d.ts.map +1 -0
  41. package/dist/state/state-manager.js +334 -0
  42. package/dist/state/state-manager.js.map +1 -0
  43. package/dist/types/events.d.ts +323 -0
  44. package/dist/types/events.d.ts.map +1 -0
  45. package/dist/types/events.js +7 -0
  46. package/dist/types/events.js.map +1 -0
  47. package/dist/types/index.d.ts +9 -0
  48. package/dist/types/index.d.ts.map +1 -0
  49. package/dist/types/index.js +7 -0
  50. package/dist/types/index.js.map +1 -0
  51. package/dist/types/plugin.d.ts +141 -0
  52. package/dist/types/plugin.d.ts.map +1 -0
  53. package/dist/types/plugin.js +8 -0
  54. package/dist/types/plugin.js.map +1 -0
  55. package/dist/types/state.d.ts +232 -0
  56. package/dist/types/state.d.ts.map +1 -0
  57. package/dist/types/state.js +8 -0
  58. package/dist/types/state.js.map +1 -0
  59. package/package.json +64 -0
package/dist/index.js ADDED
@@ -0,0 +1,2271 @@
1
+ const effectContext = {
2
+ current: null
3
+ };
4
+ let currentEffect = null;
5
+ function setCurrentEffect(effect2) {
6
+ effectContext.current = effect2;
7
+ currentEffect = effect2;
8
+ }
9
+ function getCurrentEffect() {
10
+ return effectContext.current;
11
+ }
12
+ function effect(fn) {
13
+ const execute = () => {
14
+ setCurrentEffect(execute);
15
+ try {
16
+ fn();
17
+ } catch (error) {
18
+ console.error("[Scarlett Player] Error in effect:", error);
19
+ throw error;
20
+ } finally {
21
+ setCurrentEffect(null);
22
+ }
23
+ };
24
+ execute();
25
+ return () => {
26
+ };
27
+ }
28
+ class Signal {
29
+ constructor(initialValue) {
30
+ this.subscribers = /* @__PURE__ */ new Set();
31
+ this.value = initialValue;
32
+ }
33
+ /**
34
+ * Get the current value and track dependency if called within an effect.
35
+ *
36
+ * @returns Current value
37
+ */
38
+ get() {
39
+ if (currentEffect) {
40
+ this.subscribers.add(currentEffect);
41
+ }
42
+ return this.value;
43
+ }
44
+ /**
45
+ * Set a new value and notify subscribers if changed.
46
+ *
47
+ * @param newValue - New value to set
48
+ */
49
+ set(newValue) {
50
+ if (Object.is(this.value, newValue)) {
51
+ return;
52
+ }
53
+ this.value = newValue;
54
+ this.notify();
55
+ }
56
+ /**
57
+ * Update the value using a function.
58
+ *
59
+ * @param updater - Function that receives current value and returns new value
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const count = new Signal(0);
64
+ * count.update(n => n + 1); // Increments by 1
65
+ * ```
66
+ */
67
+ update(updater) {
68
+ this.set(updater(this.value));
69
+ }
70
+ /**
71
+ * Subscribe to changes without automatic dependency tracking.
72
+ *
73
+ * @param callback - Function to call when value changes
74
+ * @returns Unsubscribe function
75
+ */
76
+ subscribe(callback) {
77
+ this.subscribers.add(callback);
78
+ return () => this.subscribers.delete(callback);
79
+ }
80
+ /**
81
+ * Notify all subscribers of a change.
82
+ * @internal
83
+ */
84
+ notify() {
85
+ this.subscribers.forEach((subscriber) => {
86
+ try {
87
+ subscriber();
88
+ } catch (error) {
89
+ console.error("[Scarlett Player] Error in signal subscriber:", error);
90
+ }
91
+ });
92
+ }
93
+ /**
94
+ * Clean up all subscriptions.
95
+ * Call this when destroying the signal.
96
+ */
97
+ destroy() {
98
+ this.subscribers.clear();
99
+ }
100
+ /**
101
+ * Get the current number of subscribers (for debugging).
102
+ * @internal
103
+ */
104
+ getSubscriberCount() {
105
+ return this.subscribers.size;
106
+ }
107
+ }
108
+ function signal(initialValue) {
109
+ return new Signal(initialValue);
110
+ }
111
+ class Computed {
112
+ constructor(computation) {
113
+ this.dirty = true;
114
+ this.subscribers = /* @__PURE__ */ new Set();
115
+ this.dependencies = /* @__PURE__ */ new Set();
116
+ this.computation = computation;
117
+ this.invalidateCallback = () => this.invalidate();
118
+ }
119
+ /**
120
+ * Get the computed value, tracking dependency if in effect context.
121
+ *
122
+ * Recomputes if dirty, otherwise returns cached value.
123
+ *
124
+ * @returns Computed value
125
+ */
126
+ get() {
127
+ if (this.dirty) {
128
+ const prevEffect = getCurrentEffect();
129
+ setCurrentEffect(this.invalidateCallback);
130
+ try {
131
+ this.value = this.computation();
132
+ this.dirty = false;
133
+ } finally {
134
+ setCurrentEffect(prevEffect);
135
+ }
136
+ }
137
+ if (currentEffect) {
138
+ this.subscribers.add(currentEffect);
139
+ }
140
+ return this.value;
141
+ }
142
+ /**
143
+ * Mark the computed value as dirty (needs recomputation).
144
+ *
145
+ * Called when a dependency changes.
146
+ * @internal
147
+ */
148
+ invalidate() {
149
+ const wasDirty = this.dirty;
150
+ this.dirty = true;
151
+ if (!wasDirty || this.subscribers.size > 0) {
152
+ this.notifySubscribers();
153
+ }
154
+ }
155
+ /**
156
+ * Subscribe to computed value changes.
157
+ *
158
+ * @param callback - Function to call when computed value may have changed
159
+ * @returns Unsubscribe function
160
+ */
161
+ subscribe(callback) {
162
+ this.subscribers.add(callback);
163
+ return () => this.subscribers.delete(callback);
164
+ }
165
+ /**
166
+ * Notify subscribers that this computed value needs recomputation.
167
+ * @internal
168
+ */
169
+ notifySubscribers() {
170
+ this.subscribers.forEach((subscriber) => {
171
+ try {
172
+ subscriber();
173
+ } catch (error) {
174
+ console.error("[Scarlett Player] Error in computed subscriber:", error);
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Clean up all subscriptions.
180
+ */
181
+ destroy() {
182
+ this.dependencies.forEach((unsub) => unsub());
183
+ this.dependencies.clear();
184
+ this.subscribers.clear();
185
+ this.value = void 0;
186
+ this.dirty = true;
187
+ }
188
+ /**
189
+ * Get the current number of subscribers (for debugging).
190
+ * @internal
191
+ */
192
+ getSubscriberCount() {
193
+ return this.subscribers.size;
194
+ }
195
+ }
196
+ function computed(computation) {
197
+ return new Computed(computation);
198
+ }
199
+ const DEFAULT_STATE = {
200
+ // Core Playback State
201
+ playbackState: "idle",
202
+ playing: false,
203
+ paused: true,
204
+ ended: false,
205
+ buffering: false,
206
+ waiting: false,
207
+ seeking: false,
208
+ // Time & Duration
209
+ currentTime: 0,
210
+ duration: NaN,
211
+ buffered: null,
212
+ bufferedAmount: 0,
213
+ // Media Info
214
+ mediaType: "unknown",
215
+ source: null,
216
+ title: "",
217
+ poster: "",
218
+ // Volume & Audio
219
+ volume: 1,
220
+ muted: false,
221
+ // Playback Controls
222
+ playbackRate: 1,
223
+ fullscreen: false,
224
+ pip: false,
225
+ controlsVisible: true,
226
+ // Quality & Tracks
227
+ qualities: [],
228
+ currentQuality: null,
229
+ audioTracks: [],
230
+ currentAudioTrack: null,
231
+ textTracks: [],
232
+ currentTextTrack: null,
233
+ // Live/DVR State (TSP features)
234
+ live: false,
235
+ liveEdge: true,
236
+ seekableRange: null,
237
+ liveLatency: 0,
238
+ lowLatencyMode: false,
239
+ // Chapters (TSP features)
240
+ chapters: [],
241
+ currentChapter: null,
242
+ // Error State
243
+ error: null,
244
+ // Network & Performance
245
+ bandwidth: 0,
246
+ autoplay: false,
247
+ loop: false,
248
+ // Casting State
249
+ airplayAvailable: false,
250
+ airplayActive: false,
251
+ chromecastAvailable: false,
252
+ chromecastActive: false,
253
+ // UI State
254
+ interacting: false,
255
+ hovering: false,
256
+ focused: false
257
+ };
258
+ class StateManager {
259
+ /**
260
+ * Create a new StateManager with default initial state.
261
+ *
262
+ * @param initialState - Optional partial initial state (merged with defaults)
263
+ */
264
+ constructor(initialState) {
265
+ this.signals = /* @__PURE__ */ new Map();
266
+ this.changeSubscribers = /* @__PURE__ */ new Set();
267
+ this.initializeSignals(initialState);
268
+ }
269
+ /**
270
+ * Initialize all state signals with default or provided values.
271
+ * @private
272
+ */
273
+ initializeSignals(overrides) {
274
+ const initialState = { ...DEFAULT_STATE, ...overrides };
275
+ for (const [key, value] of Object.entries(initialState)) {
276
+ const stateKey = key;
277
+ const stateSignal = signal(value);
278
+ stateSignal.subscribe(() => {
279
+ this.notifyChangeSubscribers(stateKey);
280
+ });
281
+ this.signals.set(stateKey, stateSignal);
282
+ }
283
+ }
284
+ /**
285
+ * Get the signal for a state property.
286
+ *
287
+ * @param key - State property key
288
+ * @returns Signal for the property
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const playingSignal = state.get('playing');
293
+ * playingSignal.get(); // false
294
+ * playingSignal.set(true);
295
+ * ```
296
+ */
297
+ get(key) {
298
+ const stateSignal = this.signals.get(key);
299
+ if (!stateSignal) {
300
+ throw new Error(`[StateManager] Unknown state key: ${key}`);
301
+ }
302
+ return stateSignal;
303
+ }
304
+ /**
305
+ * Get the current value of a state property (convenience method).
306
+ *
307
+ * @param key - State property key
308
+ * @returns Current value
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * state.getValue('playing'); // false
313
+ * ```
314
+ */
315
+ getValue(key) {
316
+ return this.get(key).get();
317
+ }
318
+ /**
319
+ * Set the value of a state property.
320
+ *
321
+ * @param key - State property key
322
+ * @param value - New value
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * state.set('playing', true);
327
+ * state.set('currentTime', 10.5);
328
+ * ```
329
+ */
330
+ set(key, value) {
331
+ this.get(key).set(value);
332
+ }
333
+ /**
334
+ * Update multiple state properties at once (batch update).
335
+ *
336
+ * More efficient than calling set() multiple times.
337
+ *
338
+ * @param updates - Partial state object with updates
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * state.update({
343
+ * playing: true,
344
+ * currentTime: 0,
345
+ * volume: 1.0,
346
+ * });
347
+ * ```
348
+ */
349
+ update(updates) {
350
+ for (const [key, value] of Object.entries(updates)) {
351
+ const stateKey = key;
352
+ if (this.signals.has(stateKey)) {
353
+ this.set(stateKey, value);
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * Subscribe to changes on a specific state property.
359
+ *
360
+ * @param key - State property key
361
+ * @param callback - Callback function receiving new value
362
+ * @returns Unsubscribe function
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * const unsub = state.subscribe('playing', (value) => {
367
+ * console.log('Playing:', value);
368
+ * });
369
+ * ```
370
+ */
371
+ subscribeToKey(key, callback) {
372
+ const stateSignal = this.get(key);
373
+ return stateSignal.subscribe(() => {
374
+ callback(stateSignal.get());
375
+ });
376
+ }
377
+ /**
378
+ * Subscribe to all state changes.
379
+ *
380
+ * Receives a StateChangeEvent for every state property change.
381
+ *
382
+ * @param callback - Callback function receiving change events
383
+ * @returns Unsubscribe function
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * const unsub = state.subscribe((event) => {
388
+ * console.log(`${event.key} changed:`, event.value);
389
+ * });
390
+ * ```
391
+ */
392
+ subscribe(callback) {
393
+ this.changeSubscribers.add(callback);
394
+ return () => this.changeSubscribers.delete(callback);
395
+ }
396
+ /**
397
+ * Notify all global change subscribers.
398
+ * @private
399
+ */
400
+ notifyChangeSubscribers(key) {
401
+ const stateSignal = this.get(key);
402
+ const value = stateSignal.get();
403
+ const event = {
404
+ key,
405
+ value,
406
+ previousValue: value
407
+ // Note: We don't track previous values in this simple impl
408
+ };
409
+ this.changeSubscribers.forEach((subscriber) => {
410
+ try {
411
+ subscriber(event);
412
+ } catch (error) {
413
+ console.error("[StateManager] Error in change subscriber:", error);
414
+ }
415
+ });
416
+ }
417
+ /**
418
+ * Reset all state to default values.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * state.reset();
423
+ * ```
424
+ */
425
+ reset() {
426
+ this.update(DEFAULT_STATE);
427
+ }
428
+ /**
429
+ * Reset a specific state property to its default value.
430
+ *
431
+ * @param key - State property key
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * state.resetKey('playing');
436
+ * ```
437
+ */
438
+ resetKey(key) {
439
+ const defaultValue = DEFAULT_STATE[key];
440
+ this.set(key, defaultValue);
441
+ }
442
+ /**
443
+ * Get a snapshot of all current state values.
444
+ *
445
+ * @returns Frozen snapshot of current state
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const snapshot = state.snapshot();
450
+ * console.log(snapshot.playing, snapshot.currentTime);
451
+ * ```
452
+ */
453
+ snapshot() {
454
+ const snapshot = {};
455
+ for (const [key, stateSignal] of this.signals) {
456
+ snapshot[key] = stateSignal.get();
457
+ }
458
+ return Object.freeze(snapshot);
459
+ }
460
+ /**
461
+ * Get the number of subscribers for a state property (for debugging).
462
+ *
463
+ * @param key - State property key
464
+ * @returns Number of subscribers
465
+ * @internal
466
+ */
467
+ getSubscriberCount(key) {
468
+ return this.signals.get(key)?.getSubscriberCount() ?? 0;
469
+ }
470
+ /**
471
+ * Destroy the state manager and cleanup all signals.
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * state.destroy();
476
+ * ```
477
+ */
478
+ destroy() {
479
+ this.signals.forEach((stateSignal) => stateSignal.destroy());
480
+ this.signals.clear();
481
+ this.changeSubscribers.clear();
482
+ }
483
+ }
484
+ const DEFAULT_OPTIONS = {
485
+ maxListeners: 100,
486
+ async: false,
487
+ interceptors: true
488
+ };
489
+ class EventBus {
490
+ /**
491
+ * Create a new EventBus.
492
+ *
493
+ * @param options - Optional configuration
494
+ */
495
+ constructor(options) {
496
+ this.listeners = /* @__PURE__ */ new Map();
497
+ this.onceListeners = /* @__PURE__ */ new Map();
498
+ this.interceptors = /* @__PURE__ */ new Map();
499
+ this.options = { ...DEFAULT_OPTIONS, ...options };
500
+ }
501
+ /**
502
+ * Subscribe to an event.
503
+ *
504
+ * @param event - Event name
505
+ * @param handler - Event handler function
506
+ * @returns Unsubscribe function
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * const unsub = events.on('playback:play', () => {
511
+ * console.log('Playing!');
512
+ * });
513
+ *
514
+ * // Later: unsubscribe
515
+ * unsub();
516
+ * ```
517
+ */
518
+ on(event, handler) {
519
+ if (!this.listeners.has(event)) {
520
+ this.listeners.set(event, /* @__PURE__ */ new Set());
521
+ }
522
+ const handlers = this.listeners.get(event);
523
+ handlers.add(handler);
524
+ this.checkMaxListeners(event);
525
+ return () => this.off(event, handler);
526
+ }
527
+ /**
528
+ * Subscribe to an event once (auto-unsubscribe after first call).
529
+ *
530
+ * @param event - Event name
531
+ * @param handler - Event handler function
532
+ * @returns Unsubscribe function
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * events.once('player:ready', () => {
537
+ * console.log('Player ready!');
538
+ * });
539
+ * ```
540
+ */
541
+ once(event, handler) {
542
+ if (!this.onceListeners.has(event)) {
543
+ this.onceListeners.set(event, /* @__PURE__ */ new Set());
544
+ }
545
+ const handlers = this.onceListeners.get(event);
546
+ handlers.add(handler);
547
+ if (!this.listeners.has(event)) {
548
+ this.listeners.set(event, /* @__PURE__ */ new Set());
549
+ }
550
+ return () => {
551
+ handlers.delete(handler);
552
+ };
553
+ }
554
+ /**
555
+ * Unsubscribe from an event.
556
+ *
557
+ * @param event - Event name
558
+ * @param handler - Event handler function to remove
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const handler = () => console.log('Playing!');
563
+ * events.on('playback:play', handler);
564
+ * events.off('playback:play', handler);
565
+ * ```
566
+ */
567
+ off(event, handler) {
568
+ const handlers = this.listeners.get(event);
569
+ if (handlers) {
570
+ handlers.delete(handler);
571
+ if (handlers.size === 0) {
572
+ this.listeners.delete(event);
573
+ }
574
+ }
575
+ const onceHandlers = this.onceListeners.get(event);
576
+ if (onceHandlers) {
577
+ onceHandlers.delete(handler);
578
+ if (onceHandlers.size === 0) {
579
+ this.onceListeners.delete(event);
580
+ }
581
+ }
582
+ }
583
+ /**
584
+ * Emit an event synchronously.
585
+ *
586
+ * Runs interceptors first, then calls all handlers.
587
+ *
588
+ * @param event - Event name
589
+ * @param payload - Event payload
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * events.emit('playback:play', undefined);
594
+ * events.emit('playback:timeupdate', { currentTime: 10.5 });
595
+ * ```
596
+ */
597
+ emit(event, payload) {
598
+ const interceptedPayload = this.runInterceptors(event, payload);
599
+ if (interceptedPayload === null) {
600
+ return;
601
+ }
602
+ const handlers = this.listeners.get(event);
603
+ if (handlers) {
604
+ const handlersArray = Array.from(handlers);
605
+ handlersArray.forEach((handler) => {
606
+ this.safeCallHandler(handler, interceptedPayload);
607
+ });
608
+ }
609
+ const onceHandlers = this.onceListeners.get(event);
610
+ if (onceHandlers) {
611
+ const handlersArray = Array.from(onceHandlers);
612
+ handlersArray.forEach((handler) => {
613
+ this.safeCallHandler(handler, interceptedPayload);
614
+ });
615
+ this.onceListeners.delete(event);
616
+ }
617
+ }
618
+ /**
619
+ * Emit an event asynchronously (next tick).
620
+ *
621
+ * @param event - Event name
622
+ * @param payload - Event payload
623
+ * @returns Promise that resolves when all handlers complete
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * await events.emitAsync('media:loaded', { src: 'video.mp4', type: 'video/mp4' });
628
+ * ```
629
+ */
630
+ async emitAsync(event, payload) {
631
+ const interceptedPayload = await this.runInterceptorsAsync(event, payload);
632
+ if (interceptedPayload === null) {
633
+ return;
634
+ }
635
+ const handlers = this.listeners.get(event);
636
+ if (handlers) {
637
+ const promises = Array.from(handlers).map(
638
+ (handler) => this.safeCallHandlerAsync(handler, interceptedPayload)
639
+ );
640
+ await Promise.all(promises);
641
+ }
642
+ const onceHandlers = this.onceListeners.get(event);
643
+ if (onceHandlers) {
644
+ const handlersArray = Array.from(onceHandlers);
645
+ const promises = handlersArray.map(
646
+ (handler) => this.safeCallHandlerAsync(handler, interceptedPayload)
647
+ );
648
+ await Promise.all(promises);
649
+ this.onceListeners.delete(event);
650
+ }
651
+ }
652
+ /**
653
+ * Add an event interceptor.
654
+ *
655
+ * Interceptors run before handlers and can modify or cancel events.
656
+ *
657
+ * @param event - Event name
658
+ * @param interceptor - Interceptor function
659
+ * @returns Remove interceptor function
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * events.intercept('playback:timeupdate', (payload) => {
664
+ * // Round time to 2 decimals
665
+ * return { currentTime: Math.round(payload.currentTime * 100) / 100 };
666
+ * });
667
+ *
668
+ * // Cancel events
669
+ * events.intercept('playback:play', (payload) => {
670
+ * if (notReady) return null; // Cancel event
671
+ * return payload;
672
+ * });
673
+ * ```
674
+ */
675
+ intercept(event, interceptor) {
676
+ if (!this.options.interceptors) {
677
+ return () => {
678
+ };
679
+ }
680
+ if (!this.interceptors.has(event)) {
681
+ this.interceptors.set(event, /* @__PURE__ */ new Set());
682
+ }
683
+ const interceptorsSet = this.interceptors.get(event);
684
+ interceptorsSet.add(interceptor);
685
+ return () => {
686
+ interceptorsSet.delete(interceptor);
687
+ if (interceptorsSet.size === 0) {
688
+ this.interceptors.delete(event);
689
+ }
690
+ };
691
+ }
692
+ /**
693
+ * Remove all listeners for an event (or all events if no event specified).
694
+ *
695
+ * @param event - Optional event name
696
+ *
697
+ * @example
698
+ * ```ts
699
+ * events.removeAllListeners('playback:play'); // Remove all playback:play listeners
700
+ * events.removeAllListeners(); // Remove ALL listeners
701
+ * ```
702
+ */
703
+ removeAllListeners(event) {
704
+ if (event) {
705
+ this.listeners.delete(event);
706
+ this.onceListeners.delete(event);
707
+ } else {
708
+ this.listeners.clear();
709
+ this.onceListeners.clear();
710
+ }
711
+ }
712
+ /**
713
+ * Get the number of listeners for an event.
714
+ *
715
+ * @param event - Event name
716
+ * @returns Number of listeners
717
+ *
718
+ * @example
719
+ * ```ts
720
+ * events.listenerCount('playback:play'); // 3
721
+ * ```
722
+ */
723
+ listenerCount(event) {
724
+ const regularCount = this.listeners.get(event)?.size ?? 0;
725
+ const onceCount = this.onceListeners.get(event)?.size ?? 0;
726
+ return regularCount + onceCount;
727
+ }
728
+ /**
729
+ * Destroy event bus and cleanup all listeners/interceptors.
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * events.destroy();
734
+ * ```
735
+ */
736
+ destroy() {
737
+ this.listeners.clear();
738
+ this.onceListeners.clear();
739
+ this.interceptors.clear();
740
+ }
741
+ /**
742
+ * Run interceptors synchronously.
743
+ * @private
744
+ */
745
+ runInterceptors(event, payload) {
746
+ if (!this.options.interceptors) {
747
+ return payload;
748
+ }
749
+ const interceptorsSet = this.interceptors.get(event);
750
+ if (!interceptorsSet || interceptorsSet.size === 0) {
751
+ return payload;
752
+ }
753
+ let currentPayload = payload;
754
+ for (const interceptor of interceptorsSet) {
755
+ try {
756
+ currentPayload = interceptor(currentPayload);
757
+ if (currentPayload === null) {
758
+ return null;
759
+ }
760
+ } catch (error) {
761
+ console.error("[EventBus] Error in interceptor:", error);
762
+ }
763
+ }
764
+ return currentPayload;
765
+ }
766
+ /**
767
+ * Run interceptors asynchronously.
768
+ * @private
769
+ */
770
+ async runInterceptorsAsync(event, payload) {
771
+ if (!this.options.interceptors) {
772
+ return payload;
773
+ }
774
+ const interceptorsSet = this.interceptors.get(event);
775
+ if (!interceptorsSet || interceptorsSet.size === 0) {
776
+ return payload;
777
+ }
778
+ let currentPayload = payload;
779
+ for (const interceptor of interceptorsSet) {
780
+ try {
781
+ const result = interceptor(currentPayload);
782
+ currentPayload = result instanceof Promise ? await result : result;
783
+ if (currentPayload === null) {
784
+ return null;
785
+ }
786
+ } catch (error) {
787
+ console.error("[EventBus] Error in interceptor:", error);
788
+ }
789
+ }
790
+ return currentPayload;
791
+ }
792
+ /**
793
+ * Safely call a handler with error handling.
794
+ * @private
795
+ */
796
+ safeCallHandler(handler, payload) {
797
+ try {
798
+ handler(payload);
799
+ } catch (error) {
800
+ console.error("[EventBus] Error in event handler:", error);
801
+ }
802
+ }
803
+ /**
804
+ * Safely call a handler asynchronously with error handling.
805
+ * @private
806
+ */
807
+ async safeCallHandlerAsync(handler, payload) {
808
+ try {
809
+ const result = handler(payload);
810
+ if (result instanceof Promise) {
811
+ await result;
812
+ }
813
+ } catch (error) {
814
+ console.error("[EventBus] Error in event handler:", error);
815
+ }
816
+ }
817
+ /**
818
+ * Check if max listeners exceeded and warn.
819
+ * @private
820
+ */
821
+ checkMaxListeners(event) {
822
+ const count = this.listenerCount(event);
823
+ if (count > this.options.maxListeners) {
824
+ console.warn(
825
+ `[EventBus] Max listeners (${this.options.maxListeners}) exceeded for event: ${event}. Current count: ${count}. This may indicate a memory leak.`
826
+ );
827
+ }
828
+ }
829
+ }
830
+ const LOG_LEVELS = ["debug", "info", "warn", "error"];
831
+ const defaultConsoleHandler = (entry) => {
832
+ const prefix = entry.scope ? `[${entry.scope}]` : "[ScarlettPlayer]";
833
+ const message = `${prefix} ${entry.message}`;
834
+ const metadata = entry.metadata ?? "";
835
+ switch (entry.level) {
836
+ case "debug":
837
+ console.debug(message, metadata);
838
+ break;
839
+ case "info":
840
+ console.info(message, metadata);
841
+ break;
842
+ case "warn":
843
+ console.warn(message, metadata);
844
+ break;
845
+ case "error":
846
+ console.error(message, metadata);
847
+ break;
848
+ }
849
+ };
850
+ class Logger {
851
+ /**
852
+ * Create a new Logger.
853
+ *
854
+ * @param options - Logger configuration
855
+ */
856
+ constructor(options) {
857
+ this.level = options?.level ?? "warn";
858
+ this.scope = options?.scope;
859
+ this.enabled = options?.enabled ?? true;
860
+ this.handlers = options?.handlers ?? [defaultConsoleHandler];
861
+ }
862
+ /**
863
+ * Create a child logger with a scope.
864
+ *
865
+ * Child loggers inherit parent settings and chain scopes.
866
+ *
867
+ * @param scope - Child logger scope
868
+ * @returns New child logger
869
+ *
870
+ * @example
871
+ * ```ts
872
+ * const logger = new Logger();
873
+ * const hlsLogger = logger.child('hls-plugin');
874
+ * hlsLogger.info('Loading manifest');
875
+ * // Output: [ScarlettPlayer:hls-plugin] Loading manifest
876
+ * ```
877
+ */
878
+ child(scope) {
879
+ return new Logger({
880
+ level: this.level,
881
+ scope: this.scope ? `${this.scope}:${scope}` : scope,
882
+ enabled: this.enabled,
883
+ handlers: this.handlers
884
+ });
885
+ }
886
+ /**
887
+ * Log a debug message.
888
+ *
889
+ * @param message - Log message
890
+ * @param metadata - Optional structured metadata
891
+ *
892
+ * @example
893
+ * ```ts
894
+ * logger.debug('Request sent', { url: '/api/video' });
895
+ * ```
896
+ */
897
+ debug(message, metadata) {
898
+ this.log("debug", message, metadata);
899
+ }
900
+ /**
901
+ * Log an info message.
902
+ *
903
+ * @param message - Log message
904
+ * @param metadata - Optional structured metadata
905
+ *
906
+ * @example
907
+ * ```ts
908
+ * logger.info('Player ready');
909
+ * ```
910
+ */
911
+ info(message, metadata) {
912
+ this.log("info", message, metadata);
913
+ }
914
+ /**
915
+ * Log a warning message.
916
+ *
917
+ * @param message - Log message
918
+ * @param metadata - Optional structured metadata
919
+ *
920
+ * @example
921
+ * ```ts
922
+ * logger.warn('Low buffer', { buffered: 2.5 });
923
+ * ```
924
+ */
925
+ warn(message, metadata) {
926
+ this.log("warn", message, metadata);
927
+ }
928
+ /**
929
+ * Log an error message.
930
+ *
931
+ * @param message - Log message
932
+ * @param metadata - Optional structured metadata
933
+ *
934
+ * @example
935
+ * ```ts
936
+ * logger.error('Playback failed', { code: 'MEDIA_ERR_DECODE' });
937
+ * ```
938
+ */
939
+ error(message, metadata) {
940
+ this.log("error", message, metadata);
941
+ }
942
+ /**
943
+ * Set the minimum log level threshold.
944
+ *
945
+ * @param level - New log level
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * logger.setLevel('debug'); // Show all logs
950
+ * logger.setLevel('error'); // Show only errors
951
+ * ```
952
+ */
953
+ setLevel(level) {
954
+ this.level = level;
955
+ }
956
+ /**
957
+ * Enable or disable logging.
958
+ *
959
+ * @param enabled - Enable flag
960
+ *
961
+ * @example
962
+ * ```ts
963
+ * logger.setEnabled(false); // Disable all logging
964
+ * ```
965
+ */
966
+ setEnabled(enabled) {
967
+ this.enabled = enabled;
968
+ }
969
+ /**
970
+ * Add a custom log handler.
971
+ *
972
+ * @param handler - Log handler function
973
+ *
974
+ * @example
975
+ * ```ts
976
+ * logger.addHandler((entry) => {
977
+ * if (entry.level === 'error') {
978
+ * sendToAnalytics(entry);
979
+ * }
980
+ * });
981
+ * ```
982
+ */
983
+ addHandler(handler) {
984
+ this.handlers.push(handler);
985
+ }
986
+ /**
987
+ * Remove a custom log handler.
988
+ *
989
+ * @param handler - Log handler function to remove
990
+ *
991
+ * @example
992
+ * ```ts
993
+ * logger.removeHandler(myHandler);
994
+ * ```
995
+ */
996
+ removeHandler(handler) {
997
+ const index = this.handlers.indexOf(handler);
998
+ if (index !== -1) {
999
+ this.handlers.splice(index, 1);
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Core logging implementation.
1004
+ * @private
1005
+ */
1006
+ log(level, message, metadata) {
1007
+ if (!this.enabled || !this.shouldLog(level)) {
1008
+ return;
1009
+ }
1010
+ const entry = {
1011
+ level,
1012
+ message,
1013
+ timestamp: Date.now(),
1014
+ scope: this.scope,
1015
+ metadata
1016
+ };
1017
+ for (const handler of this.handlers) {
1018
+ try {
1019
+ handler(entry);
1020
+ } catch (error) {
1021
+ console.error("[Logger] Handler error:", error);
1022
+ }
1023
+ }
1024
+ }
1025
+ /**
1026
+ * Check if a log level should be output.
1027
+ * @private
1028
+ */
1029
+ shouldLog(level) {
1030
+ return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.level);
1031
+ }
1032
+ }
1033
+ function createLogger(scope) {
1034
+ return new Logger({ scope });
1035
+ }
1036
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
1037
+ ErrorCode2["SOURCE_NOT_SUPPORTED"] = "SOURCE_NOT_SUPPORTED";
1038
+ ErrorCode2["SOURCE_LOAD_FAILED"] = "SOURCE_LOAD_FAILED";
1039
+ ErrorCode2["PROVIDER_NOT_FOUND"] = "PROVIDER_NOT_FOUND";
1040
+ ErrorCode2["PROVIDER_SETUP_FAILED"] = "PROVIDER_SETUP_FAILED";
1041
+ ErrorCode2["PLUGIN_SETUP_FAILED"] = "PLUGIN_SETUP_FAILED";
1042
+ ErrorCode2["PLUGIN_NOT_FOUND"] = "PLUGIN_NOT_FOUND";
1043
+ ErrorCode2["PLAYBACK_FAILED"] = "PLAYBACK_FAILED";
1044
+ ErrorCode2["MEDIA_DECODE_ERROR"] = "MEDIA_DECODE_ERROR";
1045
+ ErrorCode2["MEDIA_NETWORK_ERROR"] = "MEDIA_NETWORK_ERROR";
1046
+ ErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
1047
+ return ErrorCode2;
1048
+ })(ErrorCode || {});
1049
+ class ErrorHandler {
1050
+ /**
1051
+ * Create a new ErrorHandler.
1052
+ *
1053
+ * @param eventBus - Event bus for error emission
1054
+ * @param logger - Logger for error logging
1055
+ * @param options - Optional configuration
1056
+ */
1057
+ constructor(eventBus, logger, options) {
1058
+ this.errors = [];
1059
+ this.eventBus = eventBus;
1060
+ this.logger = logger;
1061
+ this.maxHistory = options?.maxHistory ?? 10;
1062
+ }
1063
+ /**
1064
+ * Handle an error.
1065
+ *
1066
+ * Normalizes, logs, emits, and tracks the error.
1067
+ *
1068
+ * @param error - Error to handle (native or PlayerError)
1069
+ * @param context - Optional context (what was happening)
1070
+ * @returns Normalized PlayerError
1071
+ *
1072
+ * @example
1073
+ * ```ts
1074
+ * try {
1075
+ * loadVideo();
1076
+ * } catch (error) {
1077
+ * errorHandler.handle(error as Error, { src: 'video.mp4' });
1078
+ * }
1079
+ * ```
1080
+ */
1081
+ handle(error, context) {
1082
+ const playerError = this.normalizeError(error, context);
1083
+ this.addToHistory(playerError);
1084
+ this.logError(playerError);
1085
+ this.eventBus.emit("error", playerError);
1086
+ return playerError;
1087
+ }
1088
+ /**
1089
+ * Create and handle an error from code.
1090
+ *
1091
+ * @param code - Error code
1092
+ * @param message - Error message
1093
+ * @param options - Optional error options
1094
+ * @returns Created PlayerError
1095
+ *
1096
+ * @example
1097
+ * ```ts
1098
+ * errorHandler.throw(
1099
+ * ErrorCode.SOURCE_NOT_SUPPORTED,
1100
+ * 'MP4 not supported',
1101
+ * { fatal: true, context: { type: 'video/mp4' } }
1102
+ * );
1103
+ * ```
1104
+ */
1105
+ throw(code, message, options) {
1106
+ const error = {
1107
+ code,
1108
+ message,
1109
+ fatal: options?.fatal ?? this.isFatalCode(code),
1110
+ timestamp: Date.now(),
1111
+ context: options?.context,
1112
+ originalError: options?.originalError
1113
+ };
1114
+ return this.handle(error, options?.context);
1115
+ }
1116
+ /**
1117
+ * Get error history.
1118
+ *
1119
+ * @returns Readonly copy of error history
1120
+ *
1121
+ * @example
1122
+ * ```ts
1123
+ * const history = errorHandler.getHistory();
1124
+ * console.log(`${history.length} errors occurred`);
1125
+ * ```
1126
+ */
1127
+ getHistory() {
1128
+ return [...this.errors];
1129
+ }
1130
+ /**
1131
+ * Get last error that occurred.
1132
+ *
1133
+ * @returns Last error or null if none
1134
+ *
1135
+ * @example
1136
+ * ```ts
1137
+ * const lastError = errorHandler.getLastError();
1138
+ * if (lastError?.fatal) {
1139
+ * showErrorMessage(lastError.message);
1140
+ * }
1141
+ * ```
1142
+ */
1143
+ getLastError() {
1144
+ return this.errors[this.errors.length - 1] ?? null;
1145
+ }
1146
+ /**
1147
+ * Clear error history.
1148
+ *
1149
+ * @example
1150
+ * ```ts
1151
+ * errorHandler.clearHistory();
1152
+ * ```
1153
+ */
1154
+ clearHistory() {
1155
+ this.errors = [];
1156
+ }
1157
+ /**
1158
+ * Check if any fatal errors occurred.
1159
+ *
1160
+ * @returns True if any fatal error in history
1161
+ *
1162
+ * @example
1163
+ * ```ts
1164
+ * if (errorHandler.hasFatalError()) {
1165
+ * player.reset();
1166
+ * }
1167
+ * ```
1168
+ */
1169
+ hasFatalError() {
1170
+ return this.errors.some((e) => e.fatal);
1171
+ }
1172
+ /**
1173
+ * Normalize error to PlayerError.
1174
+ * @private
1175
+ */
1176
+ normalizeError(error, context) {
1177
+ if (this.isPlayerError(error)) {
1178
+ return {
1179
+ ...error,
1180
+ context: { ...error.context, ...context }
1181
+ };
1182
+ }
1183
+ return {
1184
+ code: this.getErrorCode(error),
1185
+ message: error.message,
1186
+ fatal: this.isFatal(error),
1187
+ timestamp: Date.now(),
1188
+ context,
1189
+ originalError: error
1190
+ };
1191
+ }
1192
+ /**
1193
+ * Determine error code from native Error.
1194
+ * @private
1195
+ */
1196
+ getErrorCode(error) {
1197
+ const message = error.message.toLowerCase();
1198
+ if (message.includes("network")) {
1199
+ return "MEDIA_NETWORK_ERROR";
1200
+ }
1201
+ if (message.includes("decode")) {
1202
+ return "MEDIA_DECODE_ERROR";
1203
+ }
1204
+ if (message.includes("source")) {
1205
+ return "SOURCE_LOAD_FAILED";
1206
+ }
1207
+ if (message.includes("plugin")) {
1208
+ return "PLUGIN_SETUP_FAILED";
1209
+ }
1210
+ if (message.includes("provider")) {
1211
+ return "PROVIDER_SETUP_FAILED";
1212
+ }
1213
+ return "UNKNOWN_ERROR";
1214
+ }
1215
+ /**
1216
+ * Determine if error is fatal.
1217
+ * @private
1218
+ */
1219
+ isFatal(error) {
1220
+ return this.isFatalCode(this.getErrorCode(error));
1221
+ }
1222
+ /**
1223
+ * Determine if error code is fatal.
1224
+ * @private
1225
+ */
1226
+ isFatalCode(code) {
1227
+ const fatalCodes = [
1228
+ "SOURCE_NOT_SUPPORTED",
1229
+ "PROVIDER_NOT_FOUND",
1230
+ "MEDIA_DECODE_ERROR"
1231
+ /* MEDIA_DECODE_ERROR */
1232
+ ];
1233
+ return fatalCodes.includes(code);
1234
+ }
1235
+ /**
1236
+ * Type guard for PlayerError.
1237
+ * @private
1238
+ */
1239
+ isPlayerError(error) {
1240
+ return typeof error === "object" && error !== null && "code" in error && "message" in error && "fatal" in error && "timestamp" in error;
1241
+ }
1242
+ /**
1243
+ * Add error to history.
1244
+ * @private
1245
+ */
1246
+ addToHistory(error) {
1247
+ this.errors.push(error);
1248
+ if (this.errors.length > this.maxHistory) {
1249
+ this.errors.shift();
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Log error with appropriate level.
1254
+ * @private
1255
+ */
1256
+ logError(error) {
1257
+ const logMessage = `[${error.code}] ${error.message}`;
1258
+ if (error.fatal) {
1259
+ this.logger.error(logMessage, {
1260
+ code: error.code,
1261
+ context: error.context
1262
+ });
1263
+ } else {
1264
+ this.logger.warn(logMessage, {
1265
+ code: error.code,
1266
+ context: error.context
1267
+ });
1268
+ }
1269
+ }
1270
+ }
1271
+ class PluginAPI {
1272
+ /**
1273
+ * Create a new PluginAPI.
1274
+ *
1275
+ * @param pluginId - ID of the plugin this API belongs to
1276
+ * @param deps - Dependencies (stateManager, eventBus, logger, container, getPlugin)
1277
+ */
1278
+ constructor(pluginId, deps) {
1279
+ this.cleanupFns = [];
1280
+ this.pluginId = pluginId;
1281
+ this.stateManager = deps.stateManager;
1282
+ this.eventBus = deps.eventBus;
1283
+ this.container = deps.container;
1284
+ this.getPluginFn = deps.getPlugin;
1285
+ this.logger = {
1286
+ debug: (msg, metadata) => deps.logger.debug(`[${pluginId}] ${msg}`, metadata),
1287
+ info: (msg, metadata) => deps.logger.info(`[${pluginId}] ${msg}`, metadata),
1288
+ warn: (msg, metadata) => deps.logger.warn(`[${pluginId}] ${msg}`, metadata),
1289
+ error: (msg, metadata) => deps.logger.error(`[${pluginId}] ${msg}`, metadata)
1290
+ };
1291
+ }
1292
+ /**
1293
+ * Get a state value.
1294
+ *
1295
+ * @param key - State property key
1296
+ * @returns Current state value
1297
+ */
1298
+ getState(key) {
1299
+ return this.stateManager.getValue(key);
1300
+ }
1301
+ /**
1302
+ * Set a state value.
1303
+ *
1304
+ * @param key - State property key
1305
+ * @param value - New state value
1306
+ */
1307
+ setState(key, value) {
1308
+ this.stateManager.set(key, value);
1309
+ }
1310
+ /**
1311
+ * Subscribe to an event.
1312
+ *
1313
+ * @param event - Event name
1314
+ * @param handler - Event handler
1315
+ * @returns Unsubscribe function
1316
+ */
1317
+ on(event, handler) {
1318
+ return this.eventBus.on(event, handler);
1319
+ }
1320
+ /**
1321
+ * Unsubscribe from an event.
1322
+ *
1323
+ * @param event - Event name
1324
+ * @param handler - Event handler to remove
1325
+ */
1326
+ off(event, handler) {
1327
+ this.eventBus.off(event, handler);
1328
+ }
1329
+ /**
1330
+ * Emit an event.
1331
+ *
1332
+ * @param event - Event name
1333
+ * @param payload - Event payload
1334
+ */
1335
+ emit(event, payload) {
1336
+ this.eventBus.emit(event, payload);
1337
+ }
1338
+ /**
1339
+ * Get another plugin by ID (if ready).
1340
+ *
1341
+ * @param id - Plugin ID
1342
+ * @returns Plugin instance or null if not found/ready
1343
+ */
1344
+ getPlugin(id) {
1345
+ return this.getPluginFn(id);
1346
+ }
1347
+ /**
1348
+ * Register a cleanup function to run when plugin is destroyed.
1349
+ *
1350
+ * @param cleanup - Cleanup function
1351
+ */
1352
+ onDestroy(cleanup) {
1353
+ this.cleanupFns.push(cleanup);
1354
+ }
1355
+ /**
1356
+ * Subscribe to state changes.
1357
+ *
1358
+ * @param callback - Callback function called on any state change
1359
+ * @returns Unsubscribe function
1360
+ */
1361
+ subscribeToState(callback) {
1362
+ return this.stateManager.subscribe(callback);
1363
+ }
1364
+ /**
1365
+ * Run all registered cleanup functions.
1366
+ * Called by PluginManager when destroying the plugin.
1367
+ *
1368
+ * @internal
1369
+ */
1370
+ runCleanups() {
1371
+ for (const cleanup of this.cleanupFns) {
1372
+ try {
1373
+ cleanup();
1374
+ } catch (error) {
1375
+ this.logger.error("Cleanup function failed", { error });
1376
+ }
1377
+ }
1378
+ this.cleanupFns = [];
1379
+ }
1380
+ /**
1381
+ * Get all registered cleanup functions.
1382
+ *
1383
+ * @returns Array of cleanup functions
1384
+ * @internal
1385
+ */
1386
+ getCleanupFns() {
1387
+ return this.cleanupFns;
1388
+ }
1389
+ }
1390
+ class PluginManager {
1391
+ constructor(eventBus, stateManager, logger, options) {
1392
+ this.plugins = /* @__PURE__ */ new Map();
1393
+ this.eventBus = eventBus;
1394
+ this.stateManager = stateManager;
1395
+ this.logger = logger;
1396
+ this.container = options.container;
1397
+ }
1398
+ /** Register a plugin with optional configuration. */
1399
+ register(plugin, config) {
1400
+ if (this.plugins.has(plugin.id)) {
1401
+ throw new Error(`Plugin "${plugin.id}" is already registered`);
1402
+ }
1403
+ this.validatePlugin(plugin);
1404
+ const api = new PluginAPI(plugin.id, {
1405
+ stateManager: this.stateManager,
1406
+ eventBus: this.eventBus,
1407
+ logger: this.logger,
1408
+ container: this.container,
1409
+ getPlugin: (id) => this.getReadyPlugin(id)
1410
+ });
1411
+ this.plugins.set(plugin.id, {
1412
+ plugin,
1413
+ state: "registered",
1414
+ config,
1415
+ cleanupFns: [],
1416
+ api
1417
+ });
1418
+ this.logger.info(`Plugin registered: ${plugin.id}`);
1419
+ this.eventBus.emit("plugin:registered", { name: plugin.id, type: plugin.type });
1420
+ }
1421
+ /** Unregister a plugin. Destroys it first if active. */
1422
+ async unregister(id) {
1423
+ const record = this.plugins.get(id);
1424
+ if (!record) return;
1425
+ if (record.state === "ready") {
1426
+ await this.destroyPlugin(id);
1427
+ }
1428
+ this.plugins.delete(id);
1429
+ this.logger.info(`Plugin unregistered: ${id}`);
1430
+ }
1431
+ /** Initialize all registered plugins in dependency order. */
1432
+ async initAll() {
1433
+ const order = this.resolveDependencyOrder();
1434
+ for (const id of order) {
1435
+ await this.initPlugin(id);
1436
+ }
1437
+ }
1438
+ /** Initialize a specific plugin. */
1439
+ async initPlugin(id) {
1440
+ const record = this.plugins.get(id);
1441
+ if (!record) {
1442
+ throw new Error(`Plugin "${id}" not found`);
1443
+ }
1444
+ if (record.state === "ready") return;
1445
+ if (record.state === "initializing") {
1446
+ throw new Error(`Plugin "${id}" is already initializing (possible circular dependency)`);
1447
+ }
1448
+ for (const depId of record.plugin.dependencies || []) {
1449
+ const dep = this.plugins.get(depId);
1450
+ if (!dep) {
1451
+ throw new Error(`Plugin "${id}" depends on missing plugin "${depId}"`);
1452
+ }
1453
+ if (dep.state !== "ready") {
1454
+ await this.initPlugin(depId);
1455
+ }
1456
+ }
1457
+ try {
1458
+ record.state = "initializing";
1459
+ if (record.plugin.onStateChange) {
1460
+ const unsub = this.stateManager.subscribe(record.plugin.onStateChange.bind(record.plugin));
1461
+ record.api.onDestroy(unsub);
1462
+ }
1463
+ if (record.plugin.onError) {
1464
+ const unsub = this.eventBus.on("error", (err) => {
1465
+ record.plugin.onError?.(err.originalError || new Error(err.message));
1466
+ });
1467
+ record.api.onDestroy(unsub);
1468
+ }
1469
+ await record.plugin.init(record.api, record.config);
1470
+ record.state = "ready";
1471
+ this.logger.info(`Plugin ready: ${id}`);
1472
+ this.eventBus.emit("plugin:active", { name: id });
1473
+ } catch (error) {
1474
+ record.state = "error";
1475
+ record.error = error;
1476
+ this.logger.error(`Plugin init failed: ${id}`, { error });
1477
+ this.eventBus.emit("plugin:error", { name: id, error });
1478
+ throw error;
1479
+ }
1480
+ }
1481
+ /** Destroy all plugins in reverse dependency order. */
1482
+ async destroyAll() {
1483
+ const order = this.resolveDependencyOrder().reverse();
1484
+ for (const id of order) {
1485
+ await this.destroyPlugin(id);
1486
+ }
1487
+ }
1488
+ /** Destroy a specific plugin. */
1489
+ async destroyPlugin(id) {
1490
+ const record = this.plugins.get(id);
1491
+ if (!record || record.state !== "ready") return;
1492
+ try {
1493
+ await record.plugin.destroy();
1494
+ record.api.runCleanups();
1495
+ record.state = "registered";
1496
+ this.logger.info(`Plugin destroyed: ${id}`);
1497
+ this.eventBus.emit("plugin:destroyed", { name: id });
1498
+ } catch (error) {
1499
+ this.logger.error(`Plugin destroy failed: ${id}`, { error });
1500
+ record.state = "registered";
1501
+ }
1502
+ }
1503
+ /** Get a plugin by ID (returns any registered plugin). */
1504
+ getPlugin(id) {
1505
+ const record = this.plugins.get(id);
1506
+ return record ? record.plugin : null;
1507
+ }
1508
+ /** Get a plugin by ID only if ready (used by PluginAPI). */
1509
+ getReadyPlugin(id) {
1510
+ const record = this.plugins.get(id);
1511
+ return record?.state === "ready" ? record.plugin : null;
1512
+ }
1513
+ /** Check if a plugin is registered. */
1514
+ hasPlugin(id) {
1515
+ return this.plugins.has(id);
1516
+ }
1517
+ /** Get plugin state. */
1518
+ getPluginState(id) {
1519
+ return this.plugins.get(id)?.state ?? null;
1520
+ }
1521
+ /** Get all registered plugin IDs. */
1522
+ getPluginIds() {
1523
+ return Array.from(this.plugins.keys());
1524
+ }
1525
+ /** Get all ready plugins. */
1526
+ getReadyPlugins() {
1527
+ return Array.from(this.plugins.values()).filter((r) => r.state === "ready").map((r) => r.plugin);
1528
+ }
1529
+ /** Get plugins by type. */
1530
+ getPluginsByType(type) {
1531
+ return Array.from(this.plugins.values()).filter((r) => r.plugin.type === type).map((r) => r.plugin);
1532
+ }
1533
+ /** Select a provider plugin that can play a source. */
1534
+ selectProvider(source) {
1535
+ const providers = this.getPluginsByType("provider");
1536
+ for (const provider of providers) {
1537
+ const canPlay = provider.canPlay;
1538
+ if (typeof canPlay === "function" && canPlay(source)) {
1539
+ return provider;
1540
+ }
1541
+ }
1542
+ return null;
1543
+ }
1544
+ /** Resolve plugin initialization order using topological sort. */
1545
+ resolveDependencyOrder() {
1546
+ const visited = /* @__PURE__ */ new Set();
1547
+ const visiting = /* @__PURE__ */ new Set();
1548
+ const sorted = [];
1549
+ const visit = (id, path = []) => {
1550
+ if (visited.has(id)) return;
1551
+ if (visiting.has(id)) {
1552
+ const cycle = [...path, id].join(" -> ");
1553
+ throw new Error(`Circular dependency detected: ${cycle}`);
1554
+ }
1555
+ const record = this.plugins.get(id);
1556
+ if (!record) return;
1557
+ visiting.add(id);
1558
+ for (const depId of record.plugin.dependencies || []) {
1559
+ if (this.plugins.has(depId)) {
1560
+ visit(depId, [...path, id]);
1561
+ }
1562
+ }
1563
+ visiting.delete(id);
1564
+ visited.add(id);
1565
+ sorted.push(id);
1566
+ };
1567
+ for (const id of this.plugins.keys()) {
1568
+ visit(id);
1569
+ }
1570
+ return sorted;
1571
+ }
1572
+ /** Validate plugin has required properties. */
1573
+ validatePlugin(plugin) {
1574
+ if (!plugin.id || typeof plugin.id !== "string") {
1575
+ throw new Error("Plugin must have a valid id");
1576
+ }
1577
+ if (!plugin.name || typeof plugin.name !== "string") {
1578
+ throw new Error(`Plugin "${plugin.id}" must have a valid name`);
1579
+ }
1580
+ if (!plugin.version || typeof plugin.version !== "string") {
1581
+ throw new Error(`Plugin "${plugin.id}" must have a valid version`);
1582
+ }
1583
+ if (!plugin.type || typeof plugin.type !== "string") {
1584
+ throw new Error(`Plugin "${plugin.id}" must have a valid type`);
1585
+ }
1586
+ if (typeof plugin.init !== "function") {
1587
+ throw new Error(`Plugin "${plugin.id}" must have an init() method`);
1588
+ }
1589
+ if (typeof plugin.destroy !== "function") {
1590
+ throw new Error(`Plugin "${plugin.id}" must have a destroy() method`);
1591
+ }
1592
+ }
1593
+ }
1594
+ class ScarlettPlayer {
1595
+ /**
1596
+ * Create a new ScarlettPlayer.
1597
+ *
1598
+ * @param options - Player configuration
1599
+ */
1600
+ constructor(options) {
1601
+ this._currentProvider = null;
1602
+ this.destroyed = false;
1603
+ this.seekingWhilePlaying = false;
1604
+ this.seekResumeTimeout = null;
1605
+ if (typeof options.container === "string") {
1606
+ const el = document.querySelector(options.container);
1607
+ if (!el || !(el instanceof HTMLElement)) {
1608
+ throw new Error(`ScarlettPlayer: container not found: ${options.container}`);
1609
+ }
1610
+ this.container = el;
1611
+ } else if (options.container instanceof HTMLElement) {
1612
+ this.container = options.container;
1613
+ } else {
1614
+ throw new Error("ScarlettPlayer requires a valid HTMLElement container or CSS selector");
1615
+ }
1616
+ this.initialSrc = options.src;
1617
+ this.eventBus = new EventBus();
1618
+ this.stateManager = new StateManager({
1619
+ autoplay: options.autoplay ?? false,
1620
+ loop: options.loop ?? false,
1621
+ volume: options.volume ?? 1,
1622
+ muted: options.muted ?? false
1623
+ });
1624
+ this.logger = new Logger({
1625
+ level: options.logLevel ?? "warn",
1626
+ scope: "ScarlettPlayer"
1627
+ });
1628
+ this.errorHandler = new ErrorHandler(this.eventBus, this.logger);
1629
+ this.pluginManager = new PluginManager(
1630
+ this.eventBus,
1631
+ this.stateManager,
1632
+ this.logger,
1633
+ { container: this.container }
1634
+ );
1635
+ if (options.plugins) {
1636
+ for (const plugin of options.plugins) {
1637
+ this.pluginManager.register(plugin);
1638
+ }
1639
+ }
1640
+ this.logger.info("ScarlettPlayer initialized", {
1641
+ autoplay: options.autoplay,
1642
+ plugins: options.plugins?.length ?? 0
1643
+ });
1644
+ this.eventBus.emit("player:ready", void 0);
1645
+ }
1646
+ /**
1647
+ * Initialize the player asynchronously.
1648
+ * Initializes non-provider plugins and loads initial source if provided.
1649
+ */
1650
+ async init() {
1651
+ this.checkDestroyed();
1652
+ for (const [id, record] of this.pluginManager.plugins) {
1653
+ if (record.plugin.type !== "provider" && record.state === "registered") {
1654
+ await this.pluginManager.initPlugin(id);
1655
+ }
1656
+ }
1657
+ if (this.initialSrc) {
1658
+ await this.load(this.initialSrc);
1659
+ }
1660
+ return Promise.resolve();
1661
+ }
1662
+ /**
1663
+ * Load a media source.
1664
+ *
1665
+ * Selects appropriate provider plugin and loads the source.
1666
+ *
1667
+ * @param source - Media source URL
1668
+ * @returns Promise that resolves when source is loaded
1669
+ *
1670
+ * @example
1671
+ * ```ts
1672
+ * await player.load('video.m3u8');
1673
+ * ```
1674
+ */
1675
+ async load(source) {
1676
+ this.checkDestroyed();
1677
+ try {
1678
+ this.logger.info("Loading source", { source });
1679
+ this.stateManager.update({
1680
+ playing: false,
1681
+ paused: true,
1682
+ ended: false,
1683
+ buffering: true,
1684
+ currentTime: 0,
1685
+ duration: 0,
1686
+ bufferedAmount: 0,
1687
+ playbackState: "loading"
1688
+ });
1689
+ if (this._currentProvider) {
1690
+ const previousProviderId = this._currentProvider.id;
1691
+ this.logger.info("Destroying previous provider", { provider: previousProviderId });
1692
+ await this.pluginManager.destroyPlugin(previousProviderId);
1693
+ this._currentProvider = null;
1694
+ }
1695
+ const provider = this.pluginManager.selectProvider(source);
1696
+ if (!provider) {
1697
+ this.errorHandler.throw(
1698
+ ErrorCode.PROVIDER_NOT_FOUND,
1699
+ `No provider found for source: ${source}`,
1700
+ {
1701
+ fatal: true,
1702
+ context: { source }
1703
+ }
1704
+ );
1705
+ return;
1706
+ }
1707
+ this._currentProvider = provider;
1708
+ this.logger.info("Provider selected", { provider: provider.id });
1709
+ await this.pluginManager.initPlugin(provider.id);
1710
+ this.stateManager.set("source", { src: source, type: this.detectMimeType(source) });
1711
+ if (typeof provider.loadSource === "function") {
1712
+ await provider.loadSource(source);
1713
+ }
1714
+ if (this.stateManager.getValue("autoplay")) {
1715
+ await this.play();
1716
+ }
1717
+ } catch (error) {
1718
+ this.errorHandler.handle(error, {
1719
+ operation: "load",
1720
+ source
1721
+ });
1722
+ }
1723
+ }
1724
+ /**
1725
+ * Start playback.
1726
+ *
1727
+ * @returns Promise that resolves when playback starts
1728
+ *
1729
+ * @example
1730
+ * ```ts
1731
+ * await player.play();
1732
+ * ```
1733
+ */
1734
+ async play() {
1735
+ this.checkDestroyed();
1736
+ try {
1737
+ this.logger.debug("Play requested");
1738
+ this.stateManager.update({
1739
+ playing: true,
1740
+ paused: false,
1741
+ playbackState: "playing"
1742
+ });
1743
+ this.eventBus.emit("playback:play", void 0);
1744
+ } catch (error) {
1745
+ this.errorHandler.handle(error, { operation: "play" });
1746
+ }
1747
+ }
1748
+ /**
1749
+ * Pause playback.
1750
+ *
1751
+ * @example
1752
+ * ```ts
1753
+ * player.pause();
1754
+ * ```
1755
+ */
1756
+ pause() {
1757
+ this.checkDestroyed();
1758
+ try {
1759
+ this.logger.debug("Pause requested");
1760
+ this.seekingWhilePlaying = false;
1761
+ if (this.seekResumeTimeout !== null) {
1762
+ clearTimeout(this.seekResumeTimeout);
1763
+ this.seekResumeTimeout = null;
1764
+ }
1765
+ this.stateManager.update({
1766
+ playing: false,
1767
+ paused: true,
1768
+ playbackState: "paused"
1769
+ });
1770
+ this.eventBus.emit("playback:pause", void 0);
1771
+ } catch (error) {
1772
+ this.errorHandler.handle(error, { operation: "pause" });
1773
+ }
1774
+ }
1775
+ /**
1776
+ * Seek to a specific time.
1777
+ *
1778
+ * @param time - Time in seconds
1779
+ *
1780
+ * @example
1781
+ * ```ts
1782
+ * player.seek(30); // Seek to 30 seconds
1783
+ * ```
1784
+ */
1785
+ seek(time) {
1786
+ this.checkDestroyed();
1787
+ try {
1788
+ this.logger.debug("Seek requested", { time });
1789
+ const wasPlaying = this.stateManager.getValue("playing");
1790
+ if (wasPlaying) {
1791
+ this.seekingWhilePlaying = true;
1792
+ }
1793
+ if (this.seekResumeTimeout !== null) {
1794
+ clearTimeout(this.seekResumeTimeout);
1795
+ this.seekResumeTimeout = null;
1796
+ }
1797
+ this.eventBus.emit("playback:seeking", { time });
1798
+ this.stateManager.set("currentTime", time);
1799
+ if (this.seekingWhilePlaying) {
1800
+ this.seekResumeTimeout = setTimeout(() => {
1801
+ if (this.seekingWhilePlaying && this.stateManager.getValue("playing")) {
1802
+ this.logger.debug("Resuming playback after seek");
1803
+ this.seekingWhilePlaying = false;
1804
+ this.eventBus.emit("playback:play", void 0);
1805
+ }
1806
+ this.seekResumeTimeout = null;
1807
+ }, 300);
1808
+ }
1809
+ } catch (error) {
1810
+ this.errorHandler.handle(error, { operation: "seek", time });
1811
+ }
1812
+ }
1813
+ /**
1814
+ * Set volume.
1815
+ *
1816
+ * @param volume - Volume 0-1
1817
+ *
1818
+ * @example
1819
+ * ```ts
1820
+ * player.setVolume(0.5); // 50% volume
1821
+ * ```
1822
+ */
1823
+ setVolume(volume) {
1824
+ this.checkDestroyed();
1825
+ const clampedVolume = Math.max(0, Math.min(1, volume));
1826
+ this.stateManager.set("volume", clampedVolume);
1827
+ this.eventBus.emit("volume:change", {
1828
+ volume: clampedVolume,
1829
+ muted: this.stateManager.getValue("muted")
1830
+ });
1831
+ }
1832
+ /**
1833
+ * Set muted state.
1834
+ *
1835
+ * @param muted - Mute flag
1836
+ *
1837
+ * @example
1838
+ * ```ts
1839
+ * player.setMuted(true);
1840
+ * ```
1841
+ */
1842
+ setMuted(muted) {
1843
+ this.checkDestroyed();
1844
+ this.stateManager.set("muted", muted);
1845
+ this.eventBus.emit("volume:mute", { muted });
1846
+ }
1847
+ /**
1848
+ * Set playback rate.
1849
+ *
1850
+ * @param rate - Playback rate (e.g., 1.0 = normal, 2.0 = 2x speed)
1851
+ *
1852
+ * @example
1853
+ * ```ts
1854
+ * player.setPlaybackRate(1.5); // 1.5x speed
1855
+ * ```
1856
+ */
1857
+ setPlaybackRate(rate) {
1858
+ this.checkDestroyed();
1859
+ this.stateManager.set("playbackRate", rate);
1860
+ this.eventBus.emit("playback:ratechange", { rate });
1861
+ }
1862
+ /**
1863
+ * Set autoplay state.
1864
+ *
1865
+ * When enabled, videos will automatically play after loading.
1866
+ *
1867
+ * @param autoplay - Autoplay flag
1868
+ *
1869
+ * @example
1870
+ * ```ts
1871
+ * player.setAutoplay(true);
1872
+ * await player.load('video.mp4'); // Will auto-play
1873
+ * ```
1874
+ */
1875
+ setAutoplay(autoplay) {
1876
+ this.checkDestroyed();
1877
+ this.stateManager.set("autoplay", autoplay);
1878
+ this.logger.debug("Autoplay set", { autoplay });
1879
+ }
1880
+ /**
1881
+ * Subscribe to an event.
1882
+ *
1883
+ * @param event - Event name
1884
+ * @param handler - Event handler
1885
+ * @returns Unsubscribe function
1886
+ *
1887
+ * @example
1888
+ * ```ts
1889
+ * const unsub = player.on('playback:play', () => {
1890
+ * console.log('Playing!');
1891
+ * });
1892
+ *
1893
+ * // Later: unsubscribe
1894
+ * unsub();
1895
+ * ```
1896
+ */
1897
+ on(event, handler) {
1898
+ this.checkDestroyed();
1899
+ return this.eventBus.on(event, handler);
1900
+ }
1901
+ /**
1902
+ * Subscribe to an event once.
1903
+ *
1904
+ * @param event - Event name
1905
+ * @param handler - Event handler
1906
+ * @returns Unsubscribe function
1907
+ *
1908
+ * @example
1909
+ * ```ts
1910
+ * player.once('player:ready', () => {
1911
+ * console.log('Player ready!');
1912
+ * });
1913
+ * ```
1914
+ */
1915
+ once(event, handler) {
1916
+ this.checkDestroyed();
1917
+ return this.eventBus.once(event, handler);
1918
+ }
1919
+ /**
1920
+ * Get a plugin by name.
1921
+ *
1922
+ * @param name - Plugin name
1923
+ * @returns Plugin instance or null
1924
+ *
1925
+ * @example
1926
+ * ```ts
1927
+ * const hls = player.getPlugin('hls-plugin');
1928
+ * ```
1929
+ */
1930
+ getPlugin(name) {
1931
+ this.checkDestroyed();
1932
+ return this.pluginManager.getPlugin(name);
1933
+ }
1934
+ /**
1935
+ * Register a plugin.
1936
+ *
1937
+ * @param plugin - Plugin to register
1938
+ *
1939
+ * @example
1940
+ * ```ts
1941
+ * player.registerPlugin(myPlugin);
1942
+ * ```
1943
+ */
1944
+ registerPlugin(plugin) {
1945
+ this.checkDestroyed();
1946
+ this.pluginManager.register(plugin);
1947
+ }
1948
+ /**
1949
+ * Get current state snapshot.
1950
+ *
1951
+ * @returns Readonly state snapshot
1952
+ *
1953
+ * @example
1954
+ * ```ts
1955
+ * const state = player.getState();
1956
+ * console.log(state.playing, state.currentTime);
1957
+ * ```
1958
+ */
1959
+ getState() {
1960
+ this.checkDestroyed();
1961
+ return this.stateManager.snapshot();
1962
+ }
1963
+ // ===== Quality Methods (proxied to provider) =====
1964
+ /**
1965
+ * Get available quality levels from the current provider.
1966
+ * @returns Array of quality levels or empty array if not available
1967
+ */
1968
+ getQualities() {
1969
+ this.checkDestroyed();
1970
+ if (!this._currentProvider) return [];
1971
+ const provider = this._currentProvider;
1972
+ if (typeof provider.getLevels === "function") {
1973
+ return provider.getLevels();
1974
+ }
1975
+ return [];
1976
+ }
1977
+ /**
1978
+ * Set quality level (-1 for auto).
1979
+ * @param index - Quality level index
1980
+ */
1981
+ setQuality(index) {
1982
+ this.checkDestroyed();
1983
+ if (!this._currentProvider) {
1984
+ this.logger.warn("No provider available for quality change");
1985
+ return;
1986
+ }
1987
+ const provider = this._currentProvider;
1988
+ if (typeof provider.setLevel === "function") {
1989
+ provider.setLevel(index);
1990
+ this.eventBus.emit("quality:change", {
1991
+ quality: index === -1 ? "auto" : `level-${index}`,
1992
+ auto: index === -1
1993
+ });
1994
+ }
1995
+ }
1996
+ /**
1997
+ * Get current quality level index (-1 = auto).
1998
+ */
1999
+ getCurrentQuality() {
2000
+ this.checkDestroyed();
2001
+ if (!this._currentProvider) return -1;
2002
+ const provider = this._currentProvider;
2003
+ if (typeof provider.getCurrentLevel === "function") {
2004
+ return provider.getCurrentLevel();
2005
+ }
2006
+ return -1;
2007
+ }
2008
+ // ===== Fullscreen Methods =====
2009
+ /**
2010
+ * Request fullscreen mode.
2011
+ */
2012
+ async requestFullscreen() {
2013
+ this.checkDestroyed();
2014
+ try {
2015
+ if (this.container.requestFullscreen) {
2016
+ await this.container.requestFullscreen();
2017
+ } else if (this.container.webkitRequestFullscreen) {
2018
+ await this.container.webkitRequestFullscreen();
2019
+ }
2020
+ this.stateManager.set("fullscreen", true);
2021
+ this.eventBus.emit("fullscreen:change", { fullscreen: true });
2022
+ } catch (error) {
2023
+ this.logger.error("Fullscreen request failed", { error });
2024
+ }
2025
+ }
2026
+ /**
2027
+ * Exit fullscreen mode.
2028
+ */
2029
+ async exitFullscreen() {
2030
+ this.checkDestroyed();
2031
+ try {
2032
+ if (document.exitFullscreen) {
2033
+ await document.exitFullscreen();
2034
+ } else if (document.webkitExitFullscreen) {
2035
+ await document.webkitExitFullscreen();
2036
+ }
2037
+ this.stateManager.set("fullscreen", false);
2038
+ this.eventBus.emit("fullscreen:change", { fullscreen: false });
2039
+ } catch (error) {
2040
+ this.logger.error("Exit fullscreen failed", { error });
2041
+ }
2042
+ }
2043
+ /**
2044
+ * Toggle fullscreen mode.
2045
+ */
2046
+ async toggleFullscreen() {
2047
+ if (this.fullscreen) {
2048
+ await this.exitFullscreen();
2049
+ } else {
2050
+ await this.requestFullscreen();
2051
+ }
2052
+ }
2053
+ // ===== Casting Methods (proxied to plugins) =====
2054
+ /**
2055
+ * Request AirPlay (proxied to airplay plugin).
2056
+ */
2057
+ requestAirPlay() {
2058
+ this.checkDestroyed();
2059
+ const airplay = this.pluginManager.getPlugin("airplay");
2060
+ if (airplay && typeof airplay.showPicker === "function") {
2061
+ airplay.showPicker();
2062
+ } else {
2063
+ this.logger.warn("AirPlay plugin not available");
2064
+ }
2065
+ }
2066
+ /**
2067
+ * Request Chromecast session (proxied to chromecast plugin).
2068
+ */
2069
+ async requestChromecast() {
2070
+ this.checkDestroyed();
2071
+ const chromecast = this.pluginManager.getPlugin("chromecast");
2072
+ if (chromecast && typeof chromecast.requestSession === "function") {
2073
+ await chromecast.requestSession();
2074
+ } else {
2075
+ this.logger.warn("Chromecast plugin not available");
2076
+ }
2077
+ }
2078
+ /**
2079
+ * Stop casting (AirPlay or Chromecast).
2080
+ */
2081
+ stopCasting() {
2082
+ this.checkDestroyed();
2083
+ const airplay = this.pluginManager.getPlugin("airplay");
2084
+ if (airplay && typeof airplay.stop === "function") {
2085
+ airplay.stop();
2086
+ }
2087
+ const chromecast = this.pluginManager.getPlugin("chromecast");
2088
+ if (chromecast && typeof chromecast.stopSession === "function") {
2089
+ chromecast.stopSession();
2090
+ }
2091
+ }
2092
+ // ===== Live Stream Methods =====
2093
+ /**
2094
+ * Seek to live edge (for live streams).
2095
+ */
2096
+ seekToLive() {
2097
+ this.checkDestroyed();
2098
+ const isLive = this.stateManager.getValue("live");
2099
+ if (!isLive) {
2100
+ this.logger.warn("Not a live stream");
2101
+ return;
2102
+ }
2103
+ if (this._currentProvider) {
2104
+ const provider = this._currentProvider;
2105
+ if (typeof provider.getLiveInfo === "function") {
2106
+ const liveInfo = provider.getLiveInfo();
2107
+ if (liveInfo?.liveSyncPosition !== void 0) {
2108
+ this.seek(liveInfo.liveSyncPosition);
2109
+ return;
2110
+ }
2111
+ }
2112
+ }
2113
+ const duration = this.stateManager.getValue("duration");
2114
+ if (duration > 0) {
2115
+ this.seek(duration);
2116
+ }
2117
+ }
2118
+ /**
2119
+ * Destroy the player and cleanup all resources.
2120
+ *
2121
+ * @example
2122
+ * ```ts
2123
+ * player.destroy();
2124
+ * ```
2125
+ */
2126
+ destroy() {
2127
+ if (this.destroyed) {
2128
+ return;
2129
+ }
2130
+ this.logger.info("Destroying player");
2131
+ if (this.seekResumeTimeout !== null) {
2132
+ clearTimeout(this.seekResumeTimeout);
2133
+ this.seekResumeTimeout = null;
2134
+ }
2135
+ this.eventBus.emit("player:destroy", void 0);
2136
+ this.pluginManager.destroyAll();
2137
+ this.eventBus.destroy();
2138
+ this.stateManager.destroy();
2139
+ this.destroyed = true;
2140
+ this.logger.info("Player destroyed");
2141
+ }
2142
+ // ===== State Getters =====
2143
+ /**
2144
+ * Get playing state.
2145
+ */
2146
+ get playing() {
2147
+ return this.stateManager.getValue("playing");
2148
+ }
2149
+ /**
2150
+ * Get paused state.
2151
+ */
2152
+ get paused() {
2153
+ return this.stateManager.getValue("paused");
2154
+ }
2155
+ /**
2156
+ * Get current time in seconds.
2157
+ */
2158
+ get currentTime() {
2159
+ return this.stateManager.getValue("currentTime");
2160
+ }
2161
+ /**
2162
+ * Get duration in seconds.
2163
+ */
2164
+ get duration() {
2165
+ return this.stateManager.getValue("duration");
2166
+ }
2167
+ /**
2168
+ * Get volume (0-1).
2169
+ */
2170
+ get volume() {
2171
+ return this.stateManager.getValue("volume");
2172
+ }
2173
+ /**
2174
+ * Get muted state.
2175
+ */
2176
+ get muted() {
2177
+ return this.stateManager.getValue("muted");
2178
+ }
2179
+ /**
2180
+ * Get playback rate.
2181
+ */
2182
+ get playbackRate() {
2183
+ return this.stateManager.getValue("playbackRate");
2184
+ }
2185
+ /**
2186
+ * Get buffered amount (0-1).
2187
+ */
2188
+ get bufferedAmount() {
2189
+ return this.stateManager.getValue("bufferedAmount");
2190
+ }
2191
+ /**
2192
+ * Get current provider plugin.
2193
+ */
2194
+ get currentProvider() {
2195
+ return this._currentProvider;
2196
+ }
2197
+ /**
2198
+ * Get fullscreen state.
2199
+ */
2200
+ get fullscreen() {
2201
+ return this.stateManager.getValue("fullscreen");
2202
+ }
2203
+ /**
2204
+ * Get live stream state.
2205
+ */
2206
+ get live() {
2207
+ return this.stateManager.getValue("live");
2208
+ }
2209
+ /**
2210
+ * Get autoplay state.
2211
+ */
2212
+ get autoplay() {
2213
+ return this.stateManager.getValue("autoplay");
2214
+ }
2215
+ /**
2216
+ * Check if player is destroyed.
2217
+ * @private
2218
+ */
2219
+ checkDestroyed() {
2220
+ if (this.destroyed) {
2221
+ throw new Error("Cannot call methods on destroyed player");
2222
+ }
2223
+ }
2224
+ /**
2225
+ * Detect MIME type from source URL.
2226
+ * @private
2227
+ */
2228
+ detectMimeType(source) {
2229
+ const ext = source.split(".").pop()?.toLowerCase();
2230
+ switch (ext) {
2231
+ case "m3u8":
2232
+ return "application/x-mpegURL";
2233
+ case "mpd":
2234
+ return "application/dash+xml";
2235
+ case "mp4":
2236
+ return "video/mp4";
2237
+ case "webm":
2238
+ return "video/webm";
2239
+ case "ogg":
2240
+ return "video/ogg";
2241
+ default:
2242
+ return "video/mp4";
2243
+ }
2244
+ }
2245
+ }
2246
+ async function createPlayer(options) {
2247
+ const player = new ScarlettPlayer(options);
2248
+ await player.init();
2249
+ return player;
2250
+ }
2251
+ export {
2252
+ Computed,
2253
+ ErrorCode,
2254
+ ErrorHandler,
2255
+ EventBus,
2256
+ Logger,
2257
+ PluginAPI,
2258
+ PluginManager,
2259
+ ScarlettPlayer,
2260
+ Signal,
2261
+ StateManager,
2262
+ computed,
2263
+ createLogger,
2264
+ createPlayer,
2265
+ currentEffect,
2266
+ effect,
2267
+ getCurrentEffect,
2268
+ setCurrentEffect,
2269
+ signal
2270
+ };
2271
+ //# sourceMappingURL=index.js.map