@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.
- package/dist/compositor/compositor.d.ts.map +1 -1
- package/dist/compositor-worker.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/compositor/audio-manager.ts +411 -0
- package/src/compositor/compositor-worker.ts +158 -0
- package/src/compositor/compositor.ts +931 -0
- package/src/compositor/index.ts +19 -0
- package/src/compositor/source-pool.ts +450 -0
- package/src/compositor/types.ts +103 -0
- package/src/compositor/worker-client.ts +139 -0
- package/src/compositor/worker-types.ts +67 -0
- package/src/core/player-core.ts +273 -0
- package/src/core/state-facade.ts +98 -0
- package/src/core/track-switcher.ts +127 -0
- package/src/events/emitter.ts +137 -0
- package/src/events/types.ts +24 -0
- package/src/index.ts +124 -0
- package/src/mediafox.ts +642 -0
- package/src/playback/audio.ts +361 -0
- package/src/playback/controller.ts +446 -0
- package/src/playback/renderer.ts +1176 -0
- package/src/playback/renderers/canvas2d.ts +128 -0
- package/src/playback/renderers/factory.ts +172 -0
- package/src/playback/renderers/index.ts +5 -0
- package/src/playback/renderers/types.ts +57 -0
- package/src/playback/renderers/webgl.ts +373 -0
- package/src/playback/renderers/webgpu.ts +395 -0
- package/src/playlist/manager.ts +268 -0
- package/src/plugins/context.ts +93 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/manager.ts +482 -0
- package/src/plugins/types.ts +243 -0
- package/src/sources/manager.ts +285 -0
- package/src/sources/source.ts +84 -0
- package/src/sources/types.ts +17 -0
- package/src/state/store.ts +389 -0
- package/src/state/types.ts +18 -0
- package/src/tracks/manager.ts +421 -0
- package/src/tracks/types.ts +30 -0
- package/src/types/jassub.d.ts +1 -0
- package/src/types.ts +235 -0
- package/src/utils/async-lock.ts +26 -0
- package/src/utils/dispose.ts +28 -0
- package/src/utils/equal.ts +33 -0
- package/src/utils/errors.ts +74 -0
- package/src/utils/logger.ts +50 -0
- package/src/utils/time.ts +157 -0
package/src/mediafox.ts
ADDED
|
@@ -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
|
+
}
|