@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
@@ -0,0 +1,769 @@
1
+ /**
2
+ * ScarlettPlayer - Main player class integrating all core systems.
3
+ *
4
+ * Provides the public API for video playback, plugin management,
5
+ * state access, and event handling.
6
+ *
7
+ * Target size: ~1-1.5KB
8
+ */
9
+ import { EventBus } from './events/event-bus';
10
+ import { StateManager } from './state/state-manager';
11
+ import { Logger } from './logger';
12
+ import { ErrorHandler, ErrorCode } from './error-handler';
13
+ import { PluginManager } from './plugin-manager';
14
+ /**
15
+ * ScarlettPlayer - Lightweight, plugin-based video player.
16
+ *
17
+ * Features:
18
+ * - Plugin-based architecture
19
+ * - Reactive state management
20
+ * - Type-safe event system
21
+ * - Automatic provider selection
22
+ * - Live/DVR support (TSP)
23
+ * - Chapter/marker support (TSP)
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const player = new ScarlettPlayer({
28
+ * container: document.getElementById('player'),
29
+ * plugins: [hlsPlugin, controlsPlugin],
30
+ * });
31
+ *
32
+ * // Load and play
33
+ * await player.load('video.m3u8');
34
+ * player.play();
35
+ *
36
+ * // Listen to events
37
+ * player.on('playback:play', () => {
38
+ * console.log('Playing!');
39
+ * });
40
+ *
41
+ * // Access state
42
+ * console.log(player.playing, player.currentTime);
43
+ *
44
+ * // Cleanup
45
+ * player.destroy();
46
+ * ```
47
+ */
48
+ export class ScarlettPlayer {
49
+ /**
50
+ * Create a new ScarlettPlayer.
51
+ *
52
+ * @param options - Player configuration
53
+ */
54
+ constructor(options) {
55
+ /** Current media provider plugin */
56
+ this._currentProvider = null;
57
+ /** Player destroyed flag */
58
+ this.destroyed = false;
59
+ /** Seeking while playing flag */
60
+ this.seekingWhilePlaying = false;
61
+ /** Seek resume timeout */
62
+ this.seekResumeTimeout = null;
63
+ // Resolve container (string selector or HTMLElement)
64
+ if (typeof options.container === 'string') {
65
+ const el = document.querySelector(options.container);
66
+ if (!el || !(el instanceof HTMLElement)) {
67
+ throw new Error(`ScarlettPlayer: container not found: ${options.container}`);
68
+ }
69
+ this.container = el;
70
+ }
71
+ else if (options.container instanceof HTMLElement) {
72
+ this.container = options.container;
73
+ }
74
+ else {
75
+ throw new Error('ScarlettPlayer requires a valid HTMLElement container or CSS selector');
76
+ }
77
+ // Store initial source
78
+ this.initialSrc = options.src;
79
+ // Initialize core systems
80
+ this.eventBus = new EventBus();
81
+ this.stateManager = new StateManager({
82
+ autoplay: options.autoplay ?? false,
83
+ loop: options.loop ?? false,
84
+ volume: options.volume ?? 1.0,
85
+ muted: options.muted ?? false,
86
+ });
87
+ this.logger = new Logger({
88
+ level: options.logLevel ?? 'warn',
89
+ scope: 'ScarlettPlayer',
90
+ });
91
+ this.errorHandler = new ErrorHandler(this.eventBus, this.logger);
92
+ this.pluginManager = new PluginManager(this.eventBus, this.stateManager, this.logger, { container: this.container });
93
+ // Register plugins if provided
94
+ if (options.plugins) {
95
+ for (const plugin of options.plugins) {
96
+ this.pluginManager.register(plugin);
97
+ }
98
+ }
99
+ this.logger.info('ScarlettPlayer initialized', {
100
+ autoplay: options.autoplay,
101
+ plugins: options.plugins?.length ?? 0,
102
+ });
103
+ // Emit ready event after initialization
104
+ this.eventBus.emit('player:ready', undefined);
105
+ }
106
+ /**
107
+ * Initialize the player asynchronously.
108
+ * Initializes non-provider plugins and loads initial source if provided.
109
+ */
110
+ async init() {
111
+ this.checkDestroyed();
112
+ // Initialize non-provider plugins (UI, feature, analytics, utility)
113
+ // Providers are initialized on-demand when load() is called
114
+ for (const [id, record] of this.pluginManager.plugins) {
115
+ if (record.plugin.type !== 'provider' && record.state === 'registered') {
116
+ await this.pluginManager.initPlugin(id);
117
+ }
118
+ }
119
+ // Load initial source if provided
120
+ if (this.initialSrc) {
121
+ await this.load(this.initialSrc);
122
+ }
123
+ return Promise.resolve();
124
+ }
125
+ /**
126
+ * Load a media source.
127
+ *
128
+ * Selects appropriate provider plugin and loads the source.
129
+ *
130
+ * @param source - Media source URL
131
+ * @returns Promise that resolves when source is loaded
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * await player.load('video.m3u8');
136
+ * ```
137
+ */
138
+ async load(source) {
139
+ this.checkDestroyed();
140
+ try {
141
+ this.logger.info('Loading source', { source });
142
+ // Reset playback state when loading new source
143
+ this.stateManager.update({
144
+ playing: false,
145
+ paused: true,
146
+ ended: false,
147
+ buffering: true,
148
+ currentTime: 0,
149
+ duration: 0,
150
+ bufferedAmount: 0,
151
+ playbackState: 'loading',
152
+ });
153
+ // Destroy previous provider if switching
154
+ if (this._currentProvider) {
155
+ const previousProviderId = this._currentProvider.id;
156
+ this.logger.info('Destroying previous provider', { provider: previousProviderId });
157
+ await this.pluginManager.destroyPlugin(previousProviderId);
158
+ this._currentProvider = null;
159
+ }
160
+ // Select provider FIRST (before init)
161
+ const provider = this.pluginManager.selectProvider(source);
162
+ if (!provider) {
163
+ this.errorHandler.throw(ErrorCode.PROVIDER_NOT_FOUND, `No provider found for source: ${source}`, {
164
+ fatal: true,
165
+ context: { source },
166
+ });
167
+ return;
168
+ }
169
+ this._currentProvider = provider;
170
+ this.logger.info('Provider selected', { provider: provider.id });
171
+ // Init ONLY the selected provider (not all plugins)
172
+ await this.pluginManager.initPlugin(provider.id);
173
+ // Update state
174
+ this.stateManager.set('source', { src: source, type: this.detectMimeType(source) });
175
+ // Call provider's loadSource method and wait for it to complete
176
+ // The provider will emit media:loaded when actually ready
177
+ if (typeof provider.loadSource === 'function') {
178
+ await provider.loadSource(source);
179
+ }
180
+ // Auto-play if enabled
181
+ if (this.stateManager.getValue('autoplay')) {
182
+ await this.play();
183
+ }
184
+ }
185
+ catch (error) {
186
+ this.errorHandler.handle(error, {
187
+ operation: 'load',
188
+ source,
189
+ });
190
+ }
191
+ }
192
+ /**
193
+ * Start playback.
194
+ *
195
+ * @returns Promise that resolves when playback starts
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * await player.play();
200
+ * ```
201
+ */
202
+ async play() {
203
+ this.checkDestroyed();
204
+ try {
205
+ this.logger.debug('Play requested');
206
+ // Update state
207
+ this.stateManager.update({
208
+ playing: true,
209
+ paused: false,
210
+ playbackState: 'playing',
211
+ });
212
+ // Emit play event
213
+ this.eventBus.emit('playback:play', undefined);
214
+ }
215
+ catch (error) {
216
+ this.errorHandler.handle(error, { operation: 'play' });
217
+ }
218
+ }
219
+ /**
220
+ * Pause playback.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * player.pause();
225
+ * ```
226
+ */
227
+ pause() {
228
+ this.checkDestroyed();
229
+ try {
230
+ this.logger.debug('Pause requested');
231
+ // Clear seeking while playing flag (user explicitly paused)
232
+ this.seekingWhilePlaying = false;
233
+ if (this.seekResumeTimeout !== null) {
234
+ clearTimeout(this.seekResumeTimeout);
235
+ this.seekResumeTimeout = null;
236
+ }
237
+ // Update state
238
+ this.stateManager.update({
239
+ playing: false,
240
+ paused: true,
241
+ playbackState: 'paused',
242
+ });
243
+ // Emit pause event
244
+ this.eventBus.emit('playback:pause', undefined);
245
+ }
246
+ catch (error) {
247
+ this.errorHandler.handle(error, { operation: 'pause' });
248
+ }
249
+ }
250
+ /**
251
+ * Seek to a specific time.
252
+ *
253
+ * @param time - Time in seconds
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * player.seek(30); // Seek to 30 seconds
258
+ * ```
259
+ */
260
+ seek(time) {
261
+ this.checkDestroyed();
262
+ try {
263
+ this.logger.debug('Seek requested', { time });
264
+ // Remember if we were playing before seeking
265
+ const wasPlaying = this.stateManager.getValue('playing');
266
+ if (wasPlaying) {
267
+ this.seekingWhilePlaying = true;
268
+ }
269
+ // Clear any existing resume timeout
270
+ if (this.seekResumeTimeout !== null) {
271
+ clearTimeout(this.seekResumeTimeout);
272
+ this.seekResumeTimeout = null;
273
+ }
274
+ // Emit seeking event
275
+ this.eventBus.emit('playback:seeking', { time });
276
+ // Update state
277
+ this.stateManager.set('currentTime', time);
278
+ // If we were playing, set up a debounced resume
279
+ // This handles multiple rapid seeks gracefully
280
+ if (this.seekingWhilePlaying) {
281
+ this.seekResumeTimeout = setTimeout(() => {
282
+ if (this.seekingWhilePlaying && this.stateManager.getValue('playing')) {
283
+ this.logger.debug('Resuming playback after seek');
284
+ this.seekingWhilePlaying = false;
285
+ this.eventBus.emit('playback:play', undefined);
286
+ }
287
+ this.seekResumeTimeout = null;
288
+ }, 300); // 300ms debounce for rapid seeks
289
+ }
290
+ }
291
+ catch (error) {
292
+ this.errorHandler.handle(error, { operation: 'seek', time });
293
+ }
294
+ }
295
+ /**
296
+ * Set volume.
297
+ *
298
+ * @param volume - Volume 0-1
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * player.setVolume(0.5); // 50% volume
303
+ * ```
304
+ */
305
+ setVolume(volume) {
306
+ this.checkDestroyed();
307
+ const clampedVolume = Math.max(0, Math.min(1, volume));
308
+ this.stateManager.set('volume', clampedVolume);
309
+ this.eventBus.emit('volume:change', {
310
+ volume: clampedVolume,
311
+ muted: this.stateManager.getValue('muted'),
312
+ });
313
+ }
314
+ /**
315
+ * Set muted state.
316
+ *
317
+ * @param muted - Mute flag
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * player.setMuted(true);
322
+ * ```
323
+ */
324
+ setMuted(muted) {
325
+ this.checkDestroyed();
326
+ this.stateManager.set('muted', muted);
327
+ this.eventBus.emit('volume:mute', { muted });
328
+ }
329
+ /**
330
+ * Set playback rate.
331
+ *
332
+ * @param rate - Playback rate (e.g., 1.0 = normal, 2.0 = 2x speed)
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * player.setPlaybackRate(1.5); // 1.5x speed
337
+ * ```
338
+ */
339
+ setPlaybackRate(rate) {
340
+ this.checkDestroyed();
341
+ this.stateManager.set('playbackRate', rate);
342
+ this.eventBus.emit('playback:ratechange', { rate });
343
+ }
344
+ /**
345
+ * Set autoplay state.
346
+ *
347
+ * When enabled, videos will automatically play after loading.
348
+ *
349
+ * @param autoplay - Autoplay flag
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * player.setAutoplay(true);
354
+ * await player.load('video.mp4'); // Will auto-play
355
+ * ```
356
+ */
357
+ setAutoplay(autoplay) {
358
+ this.checkDestroyed();
359
+ this.stateManager.set('autoplay', autoplay);
360
+ this.logger.debug('Autoplay set', { autoplay });
361
+ }
362
+ /**
363
+ * Subscribe to an event.
364
+ *
365
+ * @param event - Event name
366
+ * @param handler - Event handler
367
+ * @returns Unsubscribe function
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * const unsub = player.on('playback:play', () => {
372
+ * console.log('Playing!');
373
+ * });
374
+ *
375
+ * // Later: unsubscribe
376
+ * unsub();
377
+ * ```
378
+ */
379
+ on(event, handler) {
380
+ this.checkDestroyed();
381
+ return this.eventBus.on(event, handler);
382
+ }
383
+ /**
384
+ * Subscribe to an event once.
385
+ *
386
+ * @param event - Event name
387
+ * @param handler - Event handler
388
+ * @returns Unsubscribe function
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * player.once('player:ready', () => {
393
+ * console.log('Player ready!');
394
+ * });
395
+ * ```
396
+ */
397
+ once(event, handler) {
398
+ this.checkDestroyed();
399
+ return this.eventBus.once(event, handler);
400
+ }
401
+ /**
402
+ * Get a plugin by name.
403
+ *
404
+ * @param name - Plugin name
405
+ * @returns Plugin instance or null
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const hls = player.getPlugin('hls-plugin');
410
+ * ```
411
+ */
412
+ getPlugin(name) {
413
+ this.checkDestroyed();
414
+ return this.pluginManager.getPlugin(name);
415
+ }
416
+ /**
417
+ * Register a plugin.
418
+ *
419
+ * @param plugin - Plugin to register
420
+ *
421
+ * @example
422
+ * ```ts
423
+ * player.registerPlugin(myPlugin);
424
+ * ```
425
+ */
426
+ registerPlugin(plugin) {
427
+ this.checkDestroyed();
428
+ this.pluginManager.register(plugin);
429
+ }
430
+ /**
431
+ * Get current state snapshot.
432
+ *
433
+ * @returns Readonly state snapshot
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * const state = player.getState();
438
+ * console.log(state.playing, state.currentTime);
439
+ * ```
440
+ */
441
+ getState() {
442
+ this.checkDestroyed();
443
+ return this.stateManager.snapshot();
444
+ }
445
+ // ===== Quality Methods (proxied to provider) =====
446
+ /**
447
+ * Get available quality levels from the current provider.
448
+ * @returns Array of quality levels or empty array if not available
449
+ */
450
+ getQualities() {
451
+ this.checkDestroyed();
452
+ if (!this._currentProvider)
453
+ return [];
454
+ const provider = this._currentProvider;
455
+ if (typeof provider.getLevels === 'function') {
456
+ return provider.getLevels();
457
+ }
458
+ return [];
459
+ }
460
+ /**
461
+ * Set quality level (-1 for auto).
462
+ * @param index - Quality level index
463
+ */
464
+ setQuality(index) {
465
+ this.checkDestroyed();
466
+ if (!this._currentProvider) {
467
+ this.logger.warn('No provider available for quality change');
468
+ return;
469
+ }
470
+ const provider = this._currentProvider;
471
+ if (typeof provider.setLevel === 'function') {
472
+ provider.setLevel(index);
473
+ this.eventBus.emit('quality:change', {
474
+ quality: index === -1 ? 'auto' : `level-${index}`,
475
+ auto: index === -1,
476
+ });
477
+ }
478
+ }
479
+ /**
480
+ * Get current quality level index (-1 = auto).
481
+ */
482
+ getCurrentQuality() {
483
+ this.checkDestroyed();
484
+ if (!this._currentProvider)
485
+ return -1;
486
+ const provider = this._currentProvider;
487
+ if (typeof provider.getCurrentLevel === 'function') {
488
+ return provider.getCurrentLevel();
489
+ }
490
+ return -1;
491
+ }
492
+ // ===== Fullscreen Methods =====
493
+ /**
494
+ * Request fullscreen mode.
495
+ */
496
+ async requestFullscreen() {
497
+ this.checkDestroyed();
498
+ try {
499
+ if (this.container.requestFullscreen) {
500
+ await this.container.requestFullscreen();
501
+ }
502
+ else if (this.container.webkitRequestFullscreen) {
503
+ await this.container.webkitRequestFullscreen();
504
+ }
505
+ this.stateManager.set('fullscreen', true);
506
+ this.eventBus.emit('fullscreen:change', { fullscreen: true });
507
+ }
508
+ catch (error) {
509
+ this.logger.error('Fullscreen request failed', { error });
510
+ }
511
+ }
512
+ /**
513
+ * Exit fullscreen mode.
514
+ */
515
+ async exitFullscreen() {
516
+ this.checkDestroyed();
517
+ try {
518
+ if (document.exitFullscreen) {
519
+ await document.exitFullscreen();
520
+ }
521
+ else if (document.webkitExitFullscreen) {
522
+ await document.webkitExitFullscreen();
523
+ }
524
+ this.stateManager.set('fullscreen', false);
525
+ this.eventBus.emit('fullscreen:change', { fullscreen: false });
526
+ }
527
+ catch (error) {
528
+ this.logger.error('Exit fullscreen failed', { error });
529
+ }
530
+ }
531
+ /**
532
+ * Toggle fullscreen mode.
533
+ */
534
+ async toggleFullscreen() {
535
+ if (this.fullscreen) {
536
+ await this.exitFullscreen();
537
+ }
538
+ else {
539
+ await this.requestFullscreen();
540
+ }
541
+ }
542
+ // ===== Casting Methods (proxied to plugins) =====
543
+ /**
544
+ * Request AirPlay (proxied to airplay plugin).
545
+ */
546
+ requestAirPlay() {
547
+ this.checkDestroyed();
548
+ const airplay = this.pluginManager.getPlugin('airplay');
549
+ if (airplay && typeof airplay.showPicker === 'function') {
550
+ airplay.showPicker();
551
+ }
552
+ else {
553
+ this.logger.warn('AirPlay plugin not available');
554
+ }
555
+ }
556
+ /**
557
+ * Request Chromecast session (proxied to chromecast plugin).
558
+ */
559
+ async requestChromecast() {
560
+ this.checkDestroyed();
561
+ const chromecast = this.pluginManager.getPlugin('chromecast');
562
+ if (chromecast && typeof chromecast.requestSession === 'function') {
563
+ await chromecast.requestSession();
564
+ }
565
+ else {
566
+ this.logger.warn('Chromecast plugin not available');
567
+ }
568
+ }
569
+ /**
570
+ * Stop casting (AirPlay or Chromecast).
571
+ */
572
+ stopCasting() {
573
+ this.checkDestroyed();
574
+ const airplay = this.pluginManager.getPlugin('airplay');
575
+ if (airplay && typeof airplay.stop === 'function') {
576
+ airplay.stop();
577
+ }
578
+ const chromecast = this.pluginManager.getPlugin('chromecast');
579
+ if (chromecast && typeof chromecast.stopSession === 'function') {
580
+ chromecast.stopSession();
581
+ }
582
+ }
583
+ // ===== Live Stream Methods =====
584
+ /**
585
+ * Seek to live edge (for live streams).
586
+ */
587
+ seekToLive() {
588
+ this.checkDestroyed();
589
+ // Check if stream is live
590
+ const isLive = this.stateManager.getValue('live');
591
+ if (!isLive) {
592
+ this.logger.warn('Not a live stream');
593
+ return;
594
+ }
595
+ // Try provider's getLiveInfo for live sync position
596
+ if (this._currentProvider) {
597
+ const provider = this._currentProvider;
598
+ if (typeof provider.getLiveInfo === 'function') {
599
+ const liveInfo = provider.getLiveInfo();
600
+ if (liveInfo?.liveSyncPosition !== undefined) {
601
+ this.seek(liveInfo.liveSyncPosition);
602
+ return;
603
+ }
604
+ }
605
+ }
606
+ // Fallback: seek to duration (edge)
607
+ const duration = this.stateManager.getValue('duration');
608
+ if (duration > 0) {
609
+ this.seek(duration);
610
+ }
611
+ }
612
+ /**
613
+ * Destroy the player and cleanup all resources.
614
+ *
615
+ * @example
616
+ * ```ts
617
+ * player.destroy();
618
+ * ```
619
+ */
620
+ destroy() {
621
+ if (this.destroyed) {
622
+ return;
623
+ }
624
+ this.logger.info('Destroying player');
625
+ // Clear any pending seek resume timeout
626
+ if (this.seekResumeTimeout !== null) {
627
+ clearTimeout(this.seekResumeTimeout);
628
+ this.seekResumeTimeout = null;
629
+ }
630
+ // Emit destroy event
631
+ this.eventBus.emit('player:destroy', undefined);
632
+ // Destroy plugins
633
+ this.pluginManager.destroyAll();
634
+ // Cleanup core systems
635
+ this.eventBus.destroy();
636
+ this.stateManager.destroy();
637
+ this.destroyed = true;
638
+ this.logger.info('Player destroyed');
639
+ }
640
+ // ===== State Getters =====
641
+ /**
642
+ * Get playing state.
643
+ */
644
+ get playing() {
645
+ return this.stateManager.getValue('playing');
646
+ }
647
+ /**
648
+ * Get paused state.
649
+ */
650
+ get paused() {
651
+ return this.stateManager.getValue('paused');
652
+ }
653
+ /**
654
+ * Get current time in seconds.
655
+ */
656
+ get currentTime() {
657
+ return this.stateManager.getValue('currentTime');
658
+ }
659
+ /**
660
+ * Get duration in seconds.
661
+ */
662
+ get duration() {
663
+ return this.stateManager.getValue('duration');
664
+ }
665
+ /**
666
+ * Get volume (0-1).
667
+ */
668
+ get volume() {
669
+ return this.stateManager.getValue('volume');
670
+ }
671
+ /**
672
+ * Get muted state.
673
+ */
674
+ get muted() {
675
+ return this.stateManager.getValue('muted');
676
+ }
677
+ /**
678
+ * Get playback rate.
679
+ */
680
+ get playbackRate() {
681
+ return this.stateManager.getValue('playbackRate');
682
+ }
683
+ /**
684
+ * Get buffered amount (0-1).
685
+ */
686
+ get bufferedAmount() {
687
+ return this.stateManager.getValue('bufferedAmount');
688
+ }
689
+ /**
690
+ * Get current provider plugin.
691
+ */
692
+ get currentProvider() {
693
+ return this._currentProvider;
694
+ }
695
+ /**
696
+ * Get fullscreen state.
697
+ */
698
+ get fullscreen() {
699
+ return this.stateManager.getValue('fullscreen');
700
+ }
701
+ /**
702
+ * Get live stream state.
703
+ */
704
+ get live() {
705
+ return this.stateManager.getValue('live');
706
+ }
707
+ /**
708
+ * Get autoplay state.
709
+ */
710
+ get autoplay() {
711
+ return this.stateManager.getValue('autoplay');
712
+ }
713
+ /**
714
+ * Check if player is destroyed.
715
+ * @private
716
+ */
717
+ checkDestroyed() {
718
+ if (this.destroyed) {
719
+ throw new Error('Cannot call methods on destroyed player');
720
+ }
721
+ }
722
+ /**
723
+ * Detect MIME type from source URL.
724
+ * @private
725
+ */
726
+ detectMimeType(source) {
727
+ const ext = source.split('.').pop()?.toLowerCase();
728
+ switch (ext) {
729
+ case 'm3u8':
730
+ return 'application/x-mpegURL';
731
+ case 'mpd':
732
+ return 'application/dash+xml';
733
+ case 'mp4':
734
+ return 'video/mp4';
735
+ case 'webm':
736
+ return 'video/webm';
737
+ case 'ogg':
738
+ return 'video/ogg';
739
+ default:
740
+ return 'video/mp4'; // Default fallback
741
+ }
742
+ }
743
+ }
744
+ /**
745
+ * Create a ScarlettPlayer instance and initialize it.
746
+ *
747
+ * Convenience factory function that creates and initializes
748
+ * the player in a single async call.
749
+ *
750
+ * @param options - Player configuration
751
+ * @returns Promise resolving to initialized player
752
+ *
753
+ * @example
754
+ * ```ts
755
+ * const player = await createPlayer({
756
+ * container: '#player',
757
+ * src: 'video.m3u8',
758
+ * plugins: [hlsPlugin()],
759
+ * });
760
+ *
761
+ * player.play();
762
+ * ```
763
+ */
764
+ export async function createPlayer(options) {
765
+ const player = new ScarlettPlayer(options);
766
+ await player.init();
767
+ return player;
768
+ }
769
+ //# sourceMappingURL=scarlett-player.js.map