@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.
- package/dist/compositor/source-pool.d.ts.map +1 -1
- package/dist/compositor-worker.js +1 -227
- package/dist/index.js +3 -3
- package/dist/playback/renderers/webgpu.d.ts.map +1 -1
- package/package.json +5 -4
- 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 +489 -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 +401 -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
|
@@ -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
|
+
}
|