@mediafox/core 1.2.9 → 1.2.11

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 (49) hide show
  1. package/dist/compositor/source-pool.d.ts.map +1 -1
  2. package/dist/compositor-worker.js +1 -227
  3. package/dist/index.js +3 -3
  4. package/dist/playback/renderers/webgpu.d.ts.map +1 -1
  5. package/package.json +5 -4
  6. package/src/compositor/audio-manager.ts +411 -0
  7. package/src/compositor/compositor-worker.ts +158 -0
  8. package/src/compositor/compositor.ts +931 -0
  9. package/src/compositor/index.ts +19 -0
  10. package/src/compositor/source-pool.ts +489 -0
  11. package/src/compositor/types.ts +103 -0
  12. package/src/compositor/worker-client.ts +139 -0
  13. package/src/compositor/worker-types.ts +67 -0
  14. package/src/core/player-core.ts +273 -0
  15. package/src/core/state-facade.ts +98 -0
  16. package/src/core/track-switcher.ts +127 -0
  17. package/src/events/emitter.ts +137 -0
  18. package/src/events/types.ts +24 -0
  19. package/src/index.ts +124 -0
  20. package/src/mediafox.ts +642 -0
  21. package/src/playback/audio.ts +361 -0
  22. package/src/playback/controller.ts +446 -0
  23. package/src/playback/renderer.ts +1176 -0
  24. package/src/playback/renderers/canvas2d.ts +128 -0
  25. package/src/playback/renderers/factory.ts +172 -0
  26. package/src/playback/renderers/index.ts +5 -0
  27. package/src/playback/renderers/types.ts +57 -0
  28. package/src/playback/renderers/webgl.ts +373 -0
  29. package/src/playback/renderers/webgpu.ts +401 -0
  30. package/src/playlist/manager.ts +268 -0
  31. package/src/plugins/context.ts +93 -0
  32. package/src/plugins/index.ts +15 -0
  33. package/src/plugins/manager.ts +482 -0
  34. package/src/plugins/types.ts +243 -0
  35. package/src/sources/manager.ts +285 -0
  36. package/src/sources/source.ts +84 -0
  37. package/src/sources/types.ts +17 -0
  38. package/src/state/store.ts +389 -0
  39. package/src/state/types.ts +18 -0
  40. package/src/tracks/manager.ts +421 -0
  41. package/src/tracks/types.ts +30 -0
  42. package/src/types/jassub.d.ts +1 -0
  43. package/src/types.ts +235 -0
  44. package/src/utils/async-lock.ts +26 -0
  45. package/src/utils/dispose.ts +28 -0
  46. package/src/utils/equal.ts +33 -0
  47. package/src/utils/errors.ts +74 -0
  48. package/src/utils/logger.ts +50 -0
  49. package/src/utils/time.ts +157 -0
@@ -0,0 +1,389 @@
1
+ import { RendererFactory } from '../playback/renderers';
2
+ import type { PluginManager } from '../plugins/manager';
3
+ import type {
4
+ AudioTrackInfo,
5
+ MediaInfo,
6
+ PlayerState,
7
+ PlayerStateData,
8
+ Playlist,
9
+ PlaylistItem,
10
+ PlaylistMode,
11
+ Rotation,
12
+ SubtitleTrackInfo,
13
+ TimeRange,
14
+ VideoTrackInfo,
15
+ } from '../types';
16
+ import { isDeepEqual } from '../utils/equal';
17
+ import type { StateListener, StateStore, StateUnsubscribe } from './types';
18
+
19
+ export class Store implements StateStore {
20
+ private state: PlayerStateData;
21
+ private previousState: PlayerStateData;
22
+ private listeners: Set<StateListener> = new Set();
23
+ private updateScheduled = false;
24
+ private pendingUpdates: Partial<PlayerStateData> = {};
25
+ private pendingKeys: Array<keyof PlayerStateData> = [];
26
+ private pluginManager: PluginManager | null = null;
27
+ // Pre-allocated array for listener iteration to avoid Set iterator allocation
28
+ private listenerCache: StateListener[] = [];
29
+
30
+ constructor() {
31
+ this.state = this.getInitialState();
32
+ this.previousState = { ...this.state };
33
+ }
34
+
35
+ setPluginManager(pluginManager: PluginManager): void {
36
+ this.pluginManager = pluginManager;
37
+ }
38
+
39
+ private getInitialState(): PlayerStateData {
40
+ // Detect the best available renderer
41
+ const supportedRenderers = RendererFactory.getSupportedRenderers();
42
+ const defaultRenderer = supportedRenderers[0] || 'canvas2d';
43
+
44
+ return {
45
+ state: 'idle',
46
+ currentTime: 0,
47
+ duration: 0,
48
+ buffered: [],
49
+ volume: 1,
50
+ muted: false,
51
+ playbackRate: 1,
52
+ playing: false,
53
+ paused: true,
54
+ ended: false,
55
+ seeking: false,
56
+ waiting: false,
57
+ error: null,
58
+ mediaInfo: null,
59
+ videoTracks: [],
60
+ audioTracks: [],
61
+ subtitleTracks: [],
62
+ selectedVideoTrack: null,
63
+ selectedAudioTrack: null,
64
+ selectedSubtitleTrack: null,
65
+ canPlay: false,
66
+ canPlayThrough: false,
67
+ isLive: false,
68
+ rendererType: defaultRenderer,
69
+ playlist: [],
70
+ currentPlaylistIndex: null,
71
+ playlistMode: null,
72
+ rotation: 0,
73
+ displaySize: { width: 0, height: 0 },
74
+ };
75
+ }
76
+
77
+ getState(): Readonly<PlayerStateData> {
78
+ // Return direct reference - callers should treat as immutable
79
+ // Avoids allocation on every call (was: Object.freeze({ ...this.state }))
80
+ return this.state;
81
+ }
82
+
83
+ setState(updates: Partial<PlayerStateData>): void {
84
+ // Execute beforeStateUpdate hooks
85
+ if (this.pluginManager) {
86
+ const result = this.pluginManager.executeBeforeStateUpdate(updates);
87
+ if (result === null) return; // Cancelled
88
+ updates = result;
89
+ }
90
+
91
+ // Track keys being updated to avoid Object.keys() allocation in flushUpdates
92
+ const keys = Object.keys(updates) as Array<keyof PlayerStateData>;
93
+ for (let i = 0; i < keys.length; i++) {
94
+ const key = keys[i];
95
+ if (this.pendingUpdates[key] === undefined) {
96
+ this.pendingKeys.push(key);
97
+ }
98
+ this.setPendingValue(key, updates[key]);
99
+ }
100
+
101
+ if (!this.updateScheduled) {
102
+ this.updateScheduled = true;
103
+ queueMicrotask(() => this.flushUpdates());
104
+ }
105
+ }
106
+
107
+ /** Type-safe helper to set a single pending update value */
108
+ private setPendingValue<K extends keyof PlayerStateData>(key: K, value: PlayerStateData[K] | undefined): void {
109
+ this.pendingUpdates[key] = value;
110
+ }
111
+
112
+ private flushUpdates(): void {
113
+ const keysToCheck = this.pendingKeys;
114
+ if (keysToCheck.length === 0) {
115
+ this.updateScheduled = false;
116
+ return;
117
+ }
118
+
119
+ // Check for changes BEFORE applying updates (compare pending vs current)
120
+ let hasChanges = false;
121
+ for (let i = 0; i < keysToCheck.length; i++) {
122
+ const key = keysToCheck[i];
123
+ if (!isDeepEqual(this.pendingUpdates[key], this.state[key])) {
124
+ hasChanges = true;
125
+ break;
126
+ }
127
+ }
128
+
129
+ // Copy current values to previousState only for changed keys (avoids full object spread)
130
+ // Then apply pending updates directly to state
131
+ for (let i = 0; i < keysToCheck.length; i++) {
132
+ const key = keysToCheck[i];
133
+ this.copyStateKey(key);
134
+ }
135
+ // Clear pending updates by reassigning empty object (faster than delete per key)
136
+ this.pendingUpdates = {};
137
+
138
+ // Reset tracking
139
+ this.pendingKeys.length = 0;
140
+ this.updateScheduled = false;
141
+
142
+ if (hasChanges) {
143
+ this.notifyListeners();
144
+
145
+ // Execute onStateChange hooks
146
+ if (this.pluginManager) {
147
+ this.pluginManager.executeOnStateChange(this.state, this.previousState);
148
+ }
149
+ }
150
+ }
151
+
152
+ /** Type-safe helper to copy a single key from state to previousState and apply pending update */
153
+ private copyStateKey<K extends keyof PlayerStateData>(key: K): void {
154
+ this.previousState[key] = this.state[key];
155
+ if (key in this.pendingUpdates) {
156
+ this.state[key] = this.pendingUpdates[key] as PlayerStateData[K];
157
+ }
158
+ }
159
+
160
+ subscribe(listener: StateListener): StateUnsubscribe {
161
+ this.listeners.add(listener);
162
+
163
+ // Immediately call listener with current state
164
+ listener(this.getState());
165
+
166
+ return () => {
167
+ this.listeners.delete(listener);
168
+ };
169
+ }
170
+
171
+ reset(): void {
172
+ this.state = this.getInitialState();
173
+ this.pendingUpdates = {};
174
+ this.pendingKeys.length = 0;
175
+ this.updateScheduled = false;
176
+ this.notifyListeners();
177
+ }
178
+
179
+ private notifyListeners(): void {
180
+ const currentState = this.state;
181
+ // Copy listeners to cache array to avoid Set iterator allocation
182
+ // and allow listeners to unsubscribe during iteration
183
+ const cache = this.listenerCache;
184
+ cache.length = 0;
185
+ for (const listener of this.listeners) {
186
+ cache.push(listener);
187
+ }
188
+ // Use indexed for loop (faster than forEach by ~8x)
189
+ for (let i = 0; i < cache.length; i++) {
190
+ try {
191
+ cache[i](currentState);
192
+ } catch (error) {
193
+ console.error('Error in state listener:', error);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Utility methods for common state updates
199
+ updatePlaybackState(playing: boolean): void {
200
+ const state: PlayerState = playing ? 'playing' : 'paused';
201
+ this.setState({
202
+ state,
203
+ playing,
204
+ paused: !playing,
205
+ ended: false,
206
+ });
207
+ }
208
+
209
+ updateTime(currentTime: number): void {
210
+ this.setState({ currentTime });
211
+ }
212
+
213
+ updateDuration(duration: number): void {
214
+ this.setState({ duration });
215
+ }
216
+
217
+ updateBuffered(buffered: TimeRange[]): void {
218
+ this.setState({ buffered });
219
+ }
220
+
221
+ updateVolume(volume: number, muted: boolean): void {
222
+ this.setState({ volume, muted });
223
+ }
224
+
225
+ updatePlaybackRate(playbackRate: number): void {
226
+ this.setState({ playbackRate });
227
+ }
228
+
229
+ updateMediaInfo(mediaInfo: MediaInfo | null): void {
230
+ this.setState({ mediaInfo });
231
+ }
232
+
233
+ updateTracks(
234
+ videoTracks?: VideoTrackInfo[],
235
+ audioTracks?: AudioTrackInfo[],
236
+ subtitleTracks?: SubtitleTrackInfo[]
237
+ ): void {
238
+ const updates: Partial<PlayerStateData> = {};
239
+ if (videoTracks) updates.videoTracks = videoTracks;
240
+ if (audioTracks) updates.audioTracks = audioTracks;
241
+ if (subtitleTracks) updates.subtitleTracks = subtitleTracks;
242
+ this.setState(updates);
243
+ }
244
+
245
+ updateSelectedTracks(type: 'video' | 'audio' | 'subtitle', trackId: string | null): void {
246
+ switch (type) {
247
+ case 'video':
248
+ this.setState({ selectedVideoTrack: trackId });
249
+ break;
250
+ case 'audio':
251
+ this.setState({ selectedAudioTrack: trackId });
252
+ break;
253
+ case 'subtitle':
254
+ this.setState({ selectedSubtitleTrack: trackId });
255
+ break;
256
+ }
257
+ }
258
+
259
+ updateError(error: Error | null): void {
260
+ this.setState({
261
+ error,
262
+ state: error ? 'error' : this.state.state,
263
+ });
264
+ }
265
+
266
+ updateSeekingState(seeking: boolean): void {
267
+ this.setState({ seeking });
268
+ }
269
+
270
+ updateWaitingState(waiting: boolean): void {
271
+ this.setState({ waiting });
272
+ }
273
+
274
+ updateReadyState(canPlay: boolean, canPlayThrough: boolean): void {
275
+ this.setState({
276
+ canPlay,
277
+ canPlayThrough,
278
+ state: canPlay ? 'ready' : this.state.state,
279
+ });
280
+ }
281
+
282
+ updateEndedState(ended: boolean): void {
283
+ this.setState({
284
+ ended,
285
+ playing: false,
286
+ paused: true,
287
+ state: ended ? 'ended' : this.state.state,
288
+ });
289
+ }
290
+
291
+ updateLoadingState(): void {
292
+ this.setState({
293
+ state: 'loading',
294
+ playing: false,
295
+ paused: true,
296
+ ended: false,
297
+ error: null,
298
+ });
299
+ }
300
+
301
+ updateRendererType(rendererType: import('../types').RendererType): void {
302
+ this.setState({ rendererType });
303
+ }
304
+
305
+ updateRotation(rotation: Rotation, displaySize: { width: number; height: number }): void {
306
+ this.setState({ rotation, displaySize });
307
+ }
308
+
309
+ updatePlaylist(playlist: Playlist, currentIndex: number | null = null): void {
310
+ this.setState({ playlist, currentPlaylistIndex: currentIndex });
311
+ }
312
+
313
+ updateCurrentPlaylistIndex(index: number): void {
314
+ this.setState({ currentPlaylistIndex: index });
315
+ }
316
+
317
+ updatePlaylistMode(mode: PlaylistMode): void {
318
+ this.setState({ playlistMode: mode });
319
+ }
320
+
321
+ addToPlaylist(item: PlaylistItem, insertIndex?: number): void {
322
+ const currentPlaylist = this.state.playlist;
323
+ let newPlaylist: Playlist;
324
+ let newCurrentIndex = this.state.currentPlaylistIndex;
325
+
326
+ if (insertIndex !== undefined && insertIndex >= 0 && insertIndex <= currentPlaylist.length) {
327
+ newPlaylist = [...currentPlaylist.slice(0, insertIndex), item, ...currentPlaylist.slice(insertIndex)];
328
+ if (newCurrentIndex !== null && newCurrentIndex >= insertIndex) {
329
+ newCurrentIndex += 1;
330
+ }
331
+ } else {
332
+ newPlaylist = [...currentPlaylist, item];
333
+ }
334
+
335
+ this.setState({ playlist: newPlaylist, currentPlaylistIndex: newCurrentIndex });
336
+ }
337
+
338
+ removeFromPlaylist(removeIndex: number): void {
339
+ const currentPlaylist = this.state.playlist;
340
+ if (removeIndex < 0 || removeIndex >= currentPlaylist.length) {
341
+ return;
342
+ }
343
+
344
+ const newPlaylist = currentPlaylist.filter((_, i) => i !== removeIndex);
345
+ let newCurrentIndex = this.state.currentPlaylistIndex;
346
+
347
+ if (newCurrentIndex === removeIndex) {
348
+ newCurrentIndex = newPlaylist.length > 0 ? 0 : null;
349
+ } else if (newCurrentIndex !== null && newCurrentIndex > removeIndex) {
350
+ newCurrentIndex -= 1;
351
+ }
352
+
353
+ if (newPlaylist.length === 0) {
354
+ this.setState({
355
+ playlist: newPlaylist,
356
+ currentPlaylistIndex: null,
357
+ state: 'idle',
358
+ currentTime: 0,
359
+ duration: 0,
360
+ mediaInfo: null,
361
+ videoTracks: [],
362
+ audioTracks: [],
363
+ subtitleTracks: [],
364
+ selectedVideoTrack: null,
365
+ selectedAudioTrack: null,
366
+ selectedSubtitleTrack: null,
367
+ });
368
+ } else {
369
+ this.setState({ playlist: newPlaylist, currentPlaylistIndex: newCurrentIndex });
370
+ }
371
+ }
372
+
373
+ clearPlaylist(): void {
374
+ this.setState({
375
+ playlist: [],
376
+ currentPlaylistIndex: null,
377
+ state: 'idle',
378
+ currentTime: 0,
379
+ duration: 0,
380
+ mediaInfo: null,
381
+ videoTracks: [],
382
+ audioTracks: [],
383
+ subtitleTracks: [],
384
+ selectedVideoTrack: null,
385
+ selectedAudioTrack: null,
386
+ selectedSubtitleTrack: null,
387
+ });
388
+ }
389
+ }
@@ -0,0 +1,18 @@
1
+ import type { PlayerStateData, Playlist, PlaylistItem, PlaylistMode } from '../types';
2
+
3
+ export type StateListener = (state: PlayerStateData) => void;
4
+ export type StateUnsubscribe = () => void;
5
+
6
+ export interface StateStore {
7
+ getState(): Readonly<PlayerStateData>;
8
+ setState(updates: Partial<PlayerStateData>): void;
9
+ subscribe(listener: StateListener): StateUnsubscribe;
10
+ reset(): void;
11
+ // Playlist methods
12
+ updatePlaylist(playlist: Playlist, currentIndex?: number | null): void;
13
+ updateCurrentPlaylistIndex(index: number): void;
14
+ updatePlaylistMode(mode: PlaylistMode): void;
15
+ addToPlaylist(item: PlaylistItem, insertIndex?: number): void;
16
+ removeFromPlaylist(removeIndex: number): void;
17
+ clearPlaylist(): void;
18
+ }