@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.
- package/dist/error-handler.d.ts.map +1 -0
- package/dist/error-handler.js +300 -0
- package/dist/error-handler.js.map +1 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +407 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2271 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +272 -0
- package/dist/logger.js.map +1 -0
- package/dist/plugin-api.d.ts +147 -0
- package/dist/plugin-api.d.ts.map +1 -0
- package/dist/plugin-api.js +160 -0
- package/dist/plugin-api.js.map +1 -0
- package/dist/plugin-manager.d.ts +52 -0
- package/dist/plugin-manager.d.ts.map +1 -0
- package/dist/plugin-manager.js +224 -0
- package/dist/plugin-manager.js.map +1 -0
- package/dist/scarlett-player.d.ts +404 -0
- package/dist/scarlett-player.d.ts.map +1 -0
- package/dist/scarlett-player.js +769 -0
- package/dist/scarlett-player.js.map +1 -0
- package/dist/state/computed.d.ts.map +1 -0
- package/dist/state/computed.js +134 -0
- package/dist/state/computed.js.map +1 -0
- package/dist/state/effect.d.ts.map +1 -0
- package/dist/state/effect.js +77 -0
- package/dist/state/effect.js.map +1 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +9 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/signal.d.ts.map +1 -0
- package/dist/state/signal.js +126 -0
- package/dist/state/signal.js.map +1 -0
- package/dist/state/state-manager.d.ts.map +1 -0
- package/dist/state/state-manager.js +334 -0
- package/dist/state/state-manager.js.map +1 -0
- package/dist/types/events.d.ts +323 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +7 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/plugin.d.ts +141 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +8 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/types/state.d.ts +232 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +8 -0
- package/dist/types/state.js.map +1 -0
- 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
|