@mediafox/core 1.2.8 → 1.2.10

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 (48) hide show
  1. package/dist/compositor/compositor.d.ts.map +1 -1
  2. package/dist/compositor-worker.js +1 -1
  3. package/dist/index.js +1 -1
  4. package/package.json +4 -3
  5. package/src/compositor/audio-manager.ts +411 -0
  6. package/src/compositor/compositor-worker.ts +158 -0
  7. package/src/compositor/compositor.ts +931 -0
  8. package/src/compositor/index.ts +19 -0
  9. package/src/compositor/source-pool.ts +450 -0
  10. package/src/compositor/types.ts +103 -0
  11. package/src/compositor/worker-client.ts +139 -0
  12. package/src/compositor/worker-types.ts +67 -0
  13. package/src/core/player-core.ts +273 -0
  14. package/src/core/state-facade.ts +98 -0
  15. package/src/core/track-switcher.ts +127 -0
  16. package/src/events/emitter.ts +137 -0
  17. package/src/events/types.ts +24 -0
  18. package/src/index.ts +124 -0
  19. package/src/mediafox.ts +642 -0
  20. package/src/playback/audio.ts +361 -0
  21. package/src/playback/controller.ts +446 -0
  22. package/src/playback/renderer.ts +1176 -0
  23. package/src/playback/renderers/canvas2d.ts +128 -0
  24. package/src/playback/renderers/factory.ts +172 -0
  25. package/src/playback/renderers/index.ts +5 -0
  26. package/src/playback/renderers/types.ts +57 -0
  27. package/src/playback/renderers/webgl.ts +373 -0
  28. package/src/playback/renderers/webgpu.ts +395 -0
  29. package/src/playlist/manager.ts +268 -0
  30. package/src/plugins/context.ts +93 -0
  31. package/src/plugins/index.ts +15 -0
  32. package/src/plugins/manager.ts +482 -0
  33. package/src/plugins/types.ts +243 -0
  34. package/src/sources/manager.ts +285 -0
  35. package/src/sources/source.ts +84 -0
  36. package/src/sources/types.ts +17 -0
  37. package/src/state/store.ts +389 -0
  38. package/src/state/types.ts +18 -0
  39. package/src/tracks/manager.ts +421 -0
  40. package/src/tracks/types.ts +30 -0
  41. package/src/types/jassub.d.ts +1 -0
  42. package/src/types.ts +235 -0
  43. package/src/utils/async-lock.ts +26 -0
  44. package/src/utils/dispose.ts +28 -0
  45. package/src/utils/equal.ts +33 -0
  46. package/src/utils/errors.ts +74 -0
  47. package/src/utils/logger.ts +50 -0
  48. package/src/utils/time.ts +157 -0
@@ -0,0 +1,642 @@
1
+ import { PlayerCore } from './core/player-core';
2
+ import { StateFacade } from './core/state-facade';
3
+ import { TrackSwitcher } from './core/track-switcher';
4
+ import { EventEmitter } from './events/emitter';
5
+ import type { UnsubscribeFn } from './events/types';
6
+ import { PlaybackController } from './playback/controller';
7
+ import { RendererFactory } from './playback/renderers';
8
+ import { PlaylistManager } from './playlist/manager';
9
+ import { PluginManager } from './plugins/manager';
10
+ import type { MediaFoxPlugin } from './plugins/types';
11
+ import { SourceManager } from './sources/manager';
12
+ import { Store } from './state/store';
13
+ import { TrackManager } from './tracks/manager';
14
+ import type { SubtitleTrackRegistration, SubtitleTrackResource } from './tracks/types';
15
+ import type {
16
+ AudioTrackInfo,
17
+ LoadOptions,
18
+ MediaSource,
19
+ PlayerEventListener,
20
+ PlayerEventMap,
21
+ PlayerOptions,
22
+ PlayerStateData,
23
+ PlaylistItem,
24
+ PlaylistMode,
25
+ RendererType,
26
+ Rotation,
27
+ ScreenshotOptions,
28
+ SeekOptions,
29
+ Subscription,
30
+ SubtitleTrackInfo,
31
+ VideoTrackInfo,
32
+ } from './types';
33
+ //
34
+
35
+ export class MediaFox {
36
+ private emitter: EventEmitter<PlayerEventMap>;
37
+ private store: Store;
38
+ private state: StateFacade;
39
+ private sourceManager: SourceManager;
40
+ private playbackController: PlaybackController;
41
+ private trackManager: TrackManager;
42
+ private playlistManager: PlaylistManager;
43
+ private pluginManager: PluginManager;
44
+ private options: PlayerOptions;
45
+ private disposed = false;
46
+ private getCurrentInput = () => this.sourceManager.getCurrentSource()?.input ?? null;
47
+ private trackSwitcher: TrackSwitcher;
48
+ private core: PlayerCore;
49
+
50
+ constructor(options: PlayerOptions = {}) {
51
+ this.options = {
52
+ volume: 1,
53
+ muted: false,
54
+ playbackRate: 1,
55
+ autoplay: false,
56
+ preload: 'metadata',
57
+ ...options,
58
+ };
59
+
60
+ // Initialize components
61
+ this.emitter = new EventEmitter({ maxListeners: 100 });
62
+ this.store = new Store();
63
+ this.state = new StateFacade(this.store);
64
+ this.sourceManager = new SourceManager({
65
+ maxCacheSize: options.maxCacheSize,
66
+ crossOrigin: options.crossOrigin,
67
+ });
68
+ this.playbackController = new PlaybackController({
69
+ canvas: options.renderTarget,
70
+ audioContext: options.audioContext,
71
+ volume: this.options.volume,
72
+ muted: this.options.muted,
73
+ playbackRate: this.options.playbackRate,
74
+ rendererType: this.options.renderer,
75
+ });
76
+ this.trackManager = new TrackManager();
77
+ this.playlistManager = new PlaylistManager(
78
+ this.store,
79
+ this.emitter,
80
+ async (item, autoplay) => {
81
+ await this.core.load(item.mediaSource, {
82
+ startTime: item.savedPosition ?? 0,
83
+ autoplay,
84
+ playlistItemId: item.id,
85
+ });
86
+ },
87
+ this.sourceManager
88
+ );
89
+
90
+ this.trackSwitcher = new TrackSwitcher({
91
+ sourceManager: this.sourceManager,
92
+ trackManager: this.trackManager,
93
+ playbackController: this.playbackController,
94
+ emit: this.emit.bind(this),
95
+ store: this.store,
96
+ getCurrentInput: this.getCurrentInput,
97
+ });
98
+
99
+ // Initialize plugin manager
100
+ this.pluginManager = new PluginManager(this);
101
+
102
+ this.core = new PlayerCore({
103
+ state: this.state,
104
+ sourceManager: this.sourceManager,
105
+ trackManager: this.trackManager,
106
+ playbackController: this.playbackController,
107
+ trackSwitcher: this.trackSwitcher,
108
+ emit: this.emit.bind(this),
109
+ pluginManager: this.pluginManager,
110
+ });
111
+
112
+ // Pass plugin manager to components that need it
113
+ this.playbackController.setPluginManager(this.pluginManager);
114
+ this.store.setPluginManager(this.pluginManager);
115
+
116
+ // Setup internal listeners
117
+ this.setupInternalListeners();
118
+
119
+ // Apply initial state
120
+ this.state.applyInitial(this.options.volume ?? 1, this.options.muted ?? false, this.options.playbackRate ?? 1);
121
+
122
+ // Initialize renderer type in state to requested type (default to webgpu)
123
+ // The actual renderer type will be updated when initialization completes
124
+ this.state.updateRendererType(this.options.renderer || 'webgpu');
125
+ }
126
+
127
+ private setupInternalListeners(): void {
128
+ // Playback controller listeners
129
+ this.playbackController.setTimeUpdateCallback((time) => {
130
+ this.state.updateTime(time);
131
+ this.emit('timeupdate', { currentTime: time });
132
+ });
133
+
134
+ this.playbackController.setEndedCallback(() => {
135
+ this.state.updateEndedState(true);
136
+ this.emit('ended', undefined);
137
+ const state = this.getState();
138
+
139
+ // Handle playlist modes that auto-advance or repeat
140
+ if (state.playlist.length > 0 && state.currentPlaylistIndex !== null) {
141
+ const mode = state.playlistMode;
142
+ const currentIndex = state.currentPlaylistIndex;
143
+
144
+ if (mode === 'repeat-one') {
145
+ // Restart current item
146
+ const targetIndex = currentIndex;
147
+ queueMicrotask(async () => {
148
+ try {
149
+ await this.seek(0);
150
+ await this.play();
151
+ } catch (error) {
152
+ this.emitter.emit('playlistitemerror', { index: targetIndex, error: error as Error });
153
+ }
154
+ });
155
+ } else if (mode === 'repeat') {
156
+ // Advance to next, loop to start if at end
157
+ const targetIndex = currentIndex < state.playlist.length - 1 ? currentIndex + 1 : 0;
158
+ queueMicrotask(async () => {
159
+ try {
160
+ await this.playlistManager.next();
161
+ } catch (error) {
162
+ this.emitter.emit('playlistitemerror', { index: targetIndex, error: error as Error });
163
+ }
164
+ });
165
+ } else if (mode === 'sequential' && currentIndex < state.playlist.length - 1) {
166
+ // Advance to next if not at end
167
+ const targetIndex = currentIndex + 1;
168
+ queueMicrotask(async () => {
169
+ try {
170
+ await this.playlistManager.next();
171
+ } catch (error) {
172
+ this.emitter.emit('playlistitemerror', { index: targetIndex, error: error as Error });
173
+ }
174
+ });
175
+ }
176
+ }
177
+ });
178
+
179
+ // Track manager listeners
180
+ this.trackManager.setTrackChangeListener((event) => {
181
+ this.state.updateSelectedTracks(event.type, event.newTrackId);
182
+ this.emit('trackchange', {
183
+ type: event.type,
184
+ trackId: event.newTrackId,
185
+ });
186
+ });
187
+
188
+ // Waiting/buffering callbacks
189
+ this.playbackController.setWaitingCallback(() => {
190
+ this.state.updateWaitingState(true);
191
+ this.emit('waiting', undefined);
192
+ });
193
+
194
+ this.playbackController.setPlayingCallback(() => {
195
+ const state = this.getState();
196
+ if (state.waiting) {
197
+ this.state.updateWaitingState(false);
198
+ this.emit('playing', undefined);
199
+ }
200
+ });
201
+
202
+ // Renderer callbacks
203
+ this.playbackController.setRendererChangeCallback((type) => {
204
+ this.state.updateRendererType(type);
205
+ this.emit('rendererchange', type);
206
+ });
207
+
208
+ this.playbackController.setRendererFallbackCallback((from, to) => {
209
+ this.emit('rendererfallback', { from, to });
210
+ });
211
+
212
+ // Rotation callback
213
+ this.playbackController.setRotationChangeCallback((rotation, displaySize) => {
214
+ this.store.updateRotation(rotation, displaySize);
215
+ this.emit('rotationchange', { rotation, displaySize });
216
+ });
217
+
218
+ // State change listener
219
+ this.state.subscribe((state) => {
220
+ this.emit('statechange', state);
221
+ });
222
+ }
223
+
224
+ // Main API Methods
225
+
226
+ /**
227
+ * Load a media source and prepare playback.
228
+ * Emits: loadstart, loadedmetadata, loadeddata, canplay, canplaythrough (or error)
229
+ */
230
+ async load(source: MediaSource, options: LoadOptions & { replacePlaylist?: boolean } = {}): Promise<void> {
231
+ this.checkDisposed();
232
+ const state = this.getState();
233
+
234
+ if (state.playlist.length === 0 || options.replacePlaylist) {
235
+ await this.playlistManager.loadPlaylist([{ mediaSource: source }], {
236
+ autoplay: options.autoplay ?? this.options.autoplay,
237
+ startTime: options.startTime,
238
+ });
239
+ return;
240
+ } else if (state.currentPlaylistIndex !== null && state.playlist.length > 0) {
241
+ // Replace current item
242
+ const currentIndex = state.currentPlaylistIndex;
243
+ const oldItem = state.playlist[currentIndex];
244
+ const newItem: PlaylistItem = {
245
+ ...oldItem,
246
+ mediaSource: source,
247
+ savedPosition: 0, // Reset for new source
248
+ duration: null,
249
+ };
250
+ const newPlaylist = [...state.playlist];
251
+ newPlaylist[currentIndex] = newItem;
252
+ this.store.updatePlaylist(newPlaylist, currentIndex);
253
+ this.emitter.emit('playlistchange', { playlist: newPlaylist });
254
+
255
+ // Load the new source
256
+ await this.core.load(source, {
257
+ startTime: options.startTime ?? 0,
258
+ autoplay: options.autoplay ?? this.options.autoplay,
259
+ });
260
+ return;
261
+ }
262
+
263
+ // Fallback for no playlist logic
264
+ await this.core.load(source, {
265
+ autoplay: options.autoplay ?? this.options.autoplay,
266
+ startTime: options.startTime,
267
+ });
268
+ }
269
+
270
+ /** Start playback. */
271
+ async play(): Promise<void> {
272
+ this.checkDisposed();
273
+ return this.core.play();
274
+ }
275
+
276
+ pause(): void {
277
+ this.checkDisposed();
278
+ this.core.pause();
279
+ }
280
+
281
+ /** Seek to a new time (seconds). */
282
+ async seek(time: number, _options: SeekOptions = {}): Promise<void> {
283
+ this.checkDisposed();
284
+ return this.core.seek(time);
285
+ }
286
+
287
+ /** Pause and reset to time 0. */
288
+ async stop(): Promise<void> {
289
+ this.checkDisposed();
290
+ return this.core.stop();
291
+ }
292
+
293
+ // Property getters/setters
294
+
295
+ get currentTime(): number {
296
+ return this.playbackController.getCurrentTime();
297
+ }
298
+
299
+ /**
300
+ * Sets the current playback time. Note: This is a fire-and-forget operation.
301
+ * Use seek() directly if you need to await the operation.
302
+ */
303
+ set currentTime(time: number) {
304
+ this.seek(time);
305
+ }
306
+
307
+ get duration(): number {
308
+ return this.state.getState().duration;
309
+ }
310
+
311
+ get volume(): number {
312
+ return this.playbackController.getVolume();
313
+ }
314
+
315
+ set volume(value: number) {
316
+ this.checkDisposed();
317
+ const volume = Math.max(0, Math.min(1, value));
318
+ this.playbackController.setVolume(volume);
319
+ this.state.updateVolume(volume, this.muted);
320
+ this.emit('volumechange', { volume, muted: this.muted });
321
+ }
322
+
323
+ get muted(): boolean {
324
+ return this.playbackController.isMuted();
325
+ }
326
+
327
+ set muted(value: boolean) {
328
+ this.checkDisposed();
329
+ this.playbackController.setMuted(value);
330
+ this.state.updateVolume(this.volume, value);
331
+ this.emit('volumechange', { volume: this.volume, muted: value });
332
+ }
333
+
334
+ get playbackRate(): number {
335
+ return this.playbackController.getPlaybackRate();
336
+ }
337
+
338
+ set playbackRate(value: number) {
339
+ this.checkDisposed();
340
+ const rate = Math.max(0.25, Math.min(4, value));
341
+ this.playbackController.setPlaybackRate(rate);
342
+ this.state.updatePlaybackRate(rate);
343
+ this.emit('ratechange', { playbackRate: rate });
344
+ }
345
+
346
+ get paused(): boolean {
347
+ return !this.playbackController.isPlaying();
348
+ }
349
+
350
+ get ended(): boolean {
351
+ return this.state.getState().ended;
352
+ }
353
+
354
+ get seeking(): boolean {
355
+ return this.state.getState().seeking;
356
+ }
357
+
358
+ get waiting(): boolean {
359
+ return this.state.getState().waiting;
360
+ }
361
+
362
+ // Rotation control
363
+
364
+ get rotation(): Rotation {
365
+ return this.playbackController.getRotation();
366
+ }
367
+
368
+ set rotation(value: Rotation) {
369
+ this.checkDisposed();
370
+ this.playbackController.setRotation(value);
371
+ }
372
+
373
+ get displaySize(): { width: number; height: number } {
374
+ return this.playbackController.getDisplaySize();
375
+ }
376
+
377
+ // Track management
378
+
379
+ getVideoTracks(): VideoTrackInfo[] {
380
+ return this.trackManager.getVideoTracks();
381
+ }
382
+
383
+ getAudioTracks(): AudioTrackInfo[] {
384
+ return this.trackManager.getAudioTracks();
385
+ }
386
+
387
+ getSubtitleTracks(): SubtitleTrackInfo[] {
388
+ return this.trackManager.getSubtitleTracks();
389
+ }
390
+
391
+ async selectVideoTrack(trackId: string | null): Promise<void> {
392
+ this.checkDisposed();
393
+ await this.trackSwitcher.selectVideoTrack(this.trackManager, trackId);
394
+ }
395
+
396
+ async selectAudioTrack(trackId: string | null): Promise<void> {
397
+ this.checkDisposed();
398
+ await this.trackSwitcher.selectAudioTrack(this.trackManager, trackId);
399
+ }
400
+
401
+ selectSubtitleTrack(trackId: string | null): void {
402
+ this.checkDisposed();
403
+
404
+ if (!this.trackManager.selectSubtitleTrack(trackId)) {
405
+ throw new Error(`Invalid subtitle track ID: ${trackId}`);
406
+ }
407
+ }
408
+
409
+ registerSubtitleTracks(sourceId: string, registrations: SubtitleTrackRegistration[]): void {
410
+ this.trackManager.registerSubtitleTracks(sourceId, registrations);
411
+ this.state.updateTracks(undefined, undefined, this.trackManager.getSubtitleTracks());
412
+
413
+ const currentInfo = this.state.getState().mediaInfo;
414
+ if (currentInfo) {
415
+ this.state.updateMediaInfo({
416
+ ...currentInfo,
417
+ hasSubtitles: this.trackManager.hasSubtitles(),
418
+ });
419
+ }
420
+ }
421
+
422
+ unregisterSubtitleTracks(sourceId: string): void {
423
+ this.trackManager.unregisterSubtitleTracks(sourceId);
424
+ this.state.updateTracks(undefined, undefined, this.trackManager.getSubtitleTracks());
425
+
426
+ const currentInfo = this.state.getState().mediaInfo;
427
+ if (currentInfo) {
428
+ this.state.updateMediaInfo({
429
+ ...currentInfo,
430
+ hasSubtitles: this.trackManager.hasSubtitles(),
431
+ });
432
+ }
433
+ }
434
+
435
+ async getSubtitleTrackResource(trackId: string | null): Promise<SubtitleTrackResource | null> {
436
+ return this.trackManager.getSubtitleTrackResource(trackId);
437
+ }
438
+
439
+ // Utility methods
440
+
441
+ /** Capture a screenshot of the current frame. */
442
+ async screenshot(options: ScreenshotOptions = {}): Promise<Blob | null> {
443
+ this.checkDisposed();
444
+ return this.playbackController.screenshot(options);
445
+ }
446
+
447
+ async setRenderTarget(canvas: HTMLCanvasElement | OffscreenCanvas): Promise<void> {
448
+ this.checkDisposed();
449
+ await this.playbackController.setCanvas(canvas);
450
+ }
451
+
452
+ getRenderTarget(): HTMLCanvasElement | OffscreenCanvas | null {
453
+ return this.playbackController.getCanvas();
454
+ }
455
+
456
+ /** @internal Refresh plugin overlays immediately */
457
+ refreshOverlays(): void {
458
+ this.playbackController.refreshOverlays();
459
+ }
460
+
461
+ // Playlist API
462
+ async loadPlaylist(
463
+ items: Array<MediaSource | { mediaSource: MediaSource; title?: string; poster?: string }>,
464
+ options?: { autoplay?: boolean; startTime?: number }
465
+ ): Promise<void> {
466
+ this.checkDisposed();
467
+ await this.playlistManager.loadPlaylist(items, options);
468
+ }
469
+
470
+ addToPlaylist(
471
+ item: MediaSource | { mediaSource: MediaSource; title?: string; poster?: string },
472
+ index?: number
473
+ ): void {
474
+ this.checkDisposed();
475
+ this.playlistManager.addToPlaylist(item, index);
476
+ }
477
+
478
+ async removeFromPlaylist(index: number): Promise<void> {
479
+ this.checkDisposed();
480
+ await this.playlistManager.removeFromPlaylist(index);
481
+ }
482
+
483
+ clearPlaylist(): void {
484
+ this.checkDisposed();
485
+ this.playlistManager.clearPlaylist();
486
+ }
487
+
488
+ async next(): Promise<void> {
489
+ this.checkDisposed();
490
+ await this.playlistManager.next();
491
+ }
492
+
493
+ async prev(): Promise<void> {
494
+ this.checkDisposed();
495
+ await this.playlistManager.prev();
496
+ }
497
+
498
+ async jumpTo(index: number): Promise<void> {
499
+ this.checkDisposed();
500
+ await this.playlistManager.jumpTo(index);
501
+ }
502
+
503
+ get playlist() {
504
+ return this.playlistManager.playlist;
505
+ }
506
+
507
+ get playlistIndex() {
508
+ return this.playlistManager.currentIndex;
509
+ }
510
+
511
+ get nowPlaying() {
512
+ return this.playlistManager.currentItem;
513
+ }
514
+
515
+ get playlistMode() {
516
+ return this.playlistManager.mode;
517
+ }
518
+
519
+ set playlistMode(mode: PlaylistMode) {
520
+ this.checkDisposed();
521
+ this.playlistManager.setMode(mode);
522
+ }
523
+
524
+ getRendererType(): RendererType {
525
+ return this.playbackController.getRendererType();
526
+ }
527
+
528
+ async switchRenderer(type: RendererType): Promise<void> {
529
+ this.checkDisposed();
530
+ await this.playbackController.switchRenderer(type);
531
+ }
532
+
533
+ /**
534
+ * Updates canvas backing buffer to match its CSS display size.
535
+ * Call this after changing CSS dimensions to prevent stretching.
536
+ */
537
+ updateCanvasDimensions(): void {
538
+ this.checkDisposed();
539
+ this.playbackController.updateCanvasDimensions();
540
+ }
541
+
542
+ static getSupportedRenderers(): RendererType[] {
543
+ return RendererFactory.getSupportedRenderers();
544
+ }
545
+
546
+ getState(): Readonly<PlayerStateData> {
547
+ return this.state.getState();
548
+ }
549
+
550
+ /** Subscribe to state changes; returns an unsubscribe handle. */
551
+ subscribe(listener: (state: PlayerStateData) => void): Subscription {
552
+ const unsubscribe = this.state.subscribe(listener);
553
+ return { unsubscribe };
554
+ }
555
+
556
+ // Event handling
557
+
558
+ on<K extends keyof PlayerEventMap>(event: K, listener: PlayerEventListener<K>): UnsubscribeFn {
559
+ return this.emitter.on(event, listener);
560
+ }
561
+
562
+ once<K extends keyof PlayerEventMap>(event: K, listener: PlayerEventListener<K>): UnsubscribeFn {
563
+ return this.emitter.once(event, listener);
564
+ }
565
+
566
+ off<K extends keyof PlayerEventMap>(event: K, listener?: PlayerEventListener<K>): void {
567
+ this.emitter.off(event, listener);
568
+ }
569
+
570
+ // Plugin API
571
+
572
+ /**
573
+ * Install a plugin.
574
+ * @param plugin The plugin to install
575
+ */
576
+ async use(plugin: MediaFoxPlugin): Promise<void> {
577
+ this.checkDisposed();
578
+ await this.pluginManager.install(plugin);
579
+
580
+ // Rebuild audio graph if plugin has audio hooks
581
+ if (plugin.hooks?.audio) {
582
+ this.playbackController.rebuildAudioGraph();
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Uninstall a plugin by name.
588
+ * @param name The name of the plugin to uninstall
589
+ */
590
+ async unuse(name: string): Promise<void> {
591
+ this.checkDisposed();
592
+ await this.pluginManager.uninstall(name);
593
+
594
+ // Always rebuild audio graph after uninstalling (cheaper than checking)
595
+ this.playbackController.rebuildAudioGraph();
596
+ }
597
+
598
+ private emit<K extends keyof PlayerEventMap>(event: K, data: PlayerEventMap[K]): void {
599
+ // Execute beforeEvent hooks
600
+ const result = this.pluginManager.executeBeforeEvent(event, data);
601
+ if (result?.cancel) return;
602
+
603
+ // Use modified data if provided
604
+ const finalData = result?.data ?? data;
605
+
606
+ // Emit the event
607
+ this.emitter.emit(event, finalData);
608
+
609
+ // Execute afterEvent hooks
610
+ this.pluginManager.executeAfterEvent(event, finalData);
611
+ }
612
+
613
+ private checkDisposed(): void {
614
+ if (this.disposed) {
615
+ throw new Error('Player has been disposed');
616
+ }
617
+ }
618
+
619
+ // Cleanup
620
+
621
+ dispose(): void {
622
+ if (this.disposed) return;
623
+
624
+ this.disposed = true;
625
+
626
+ // Dispose plugins first
627
+ void this.pluginManager.dispose();
628
+
629
+ // Dispose components
630
+ this.playbackController.dispose();
631
+ this.trackManager.dispose();
632
+ this.playlistManager?.dispose(); // If manager has dispose
633
+ this.sourceManager.dispose();
634
+ this.state.reset();
635
+ this.emitter.removeAllListeners();
636
+ }
637
+
638
+ destroy(): void {
639
+ this.dispose();
640
+ this.playbackController.destroy();
641
+ }
642
+ }