@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,361 @@
1
+ import {
2
+ AudioBufferSink,
3
+ type AudioSample,
4
+ AudioSampleSink,
5
+ type InputAudioTrack,
6
+ type WrappedAudioBuffer,
7
+ } from 'mediabunny';
8
+ import type { PluginManager } from '../plugins/manager';
9
+
10
+ export interface AudioManagerOptions {
11
+ audioContext?: AudioContext;
12
+ volume?: number;
13
+ muted?: boolean;
14
+ }
15
+
16
+ export class AudioManager {
17
+ private audioContext: AudioContext;
18
+ private gainNode: GainNode | null = null;
19
+ private outputNode: AudioNode | null = null;
20
+ private bufferSink: AudioBufferSink | null = null;
21
+ private sampleSink: AudioSampleSink | null = null;
22
+ private bufferIterator: AsyncGenerator<WrappedAudioBuffer, void, unknown> | null = null;
23
+ private queuedNodes: Set<AudioBufferSourceNode> = new Set();
24
+ private startContextTime = 0;
25
+ private startMediaTime = 0;
26
+ private pauseTime = 0;
27
+ private playing = false;
28
+ private volume = 1;
29
+ private muted = false;
30
+ private disposed = false;
31
+ private playbackId = 0;
32
+ private playbackRate = 1;
33
+ private pluginManager: PluginManager | null = null;
34
+
35
+ constructor(options: AudioManagerOptions = {}) {
36
+ // Create or use provided AudioContext
37
+ if (options.audioContext) {
38
+ this.audioContext = options.audioContext;
39
+ } else {
40
+ const windowGlobal = window as typeof window & {
41
+ webkitAudioContext?: typeof AudioContext;
42
+ };
43
+ const AudioContextClass = windowGlobal.AudioContext || windowGlobal.webkitAudioContext;
44
+ this.audioContext = new AudioContextClass();
45
+ }
46
+
47
+ // Set initial volume and mute state
48
+ this.volume = options.volume ?? 1;
49
+ this.muted = options.muted ?? false;
50
+
51
+ this.setupAudioGraph();
52
+ }
53
+
54
+ private setupAudioGraph(): void {
55
+ this.gainNode = this.audioContext.createGain();
56
+
57
+ // Execute onAudioNode hooks to allow plugins to modify the audio graph
58
+ if (this.pluginManager) {
59
+ const result = this.pluginManager.executeOnAudioNode(this.audioContext, this.gainNode);
60
+ this.outputNode = result;
61
+ } else {
62
+ this.outputNode = this.gainNode;
63
+ }
64
+
65
+ this.outputNode.connect(this.audioContext.destination);
66
+ this.updateGain();
67
+ }
68
+
69
+ setPluginManager(pluginManager: PluginManager): void {
70
+ this.pluginManager = pluginManager;
71
+ this.rebuildAudioGraph();
72
+ }
73
+
74
+ /**
75
+ * Rebuild the audio graph with current plugin hooks.
76
+ * Call this after installing/uninstalling audio plugins.
77
+ */
78
+ rebuildAudioGraph(): void {
79
+ if (!this.gainNode) return;
80
+
81
+ // Disconnect current output
82
+ this.outputNode?.disconnect();
83
+
84
+ // Execute plugin hooks to get new output node
85
+ if (this.pluginManager) {
86
+ const result = this.pluginManager.executeOnAudioNode(this.audioContext, this.gainNode);
87
+ this.outputNode = result;
88
+ } else {
89
+ this.outputNode = this.gainNode;
90
+ }
91
+
92
+ // Reconnect to destination
93
+ this.outputNode.connect(this.audioContext.destination);
94
+ }
95
+
96
+ async setAudioTrack(track: InputAudioTrack): Promise<void> {
97
+ this.dispose();
98
+
99
+ // Check if we can decode before throwing
100
+ if (track.codec === null) {
101
+ throw new Error(`Unsupported audio codec`);
102
+ }
103
+
104
+ const canDecode = await track.canDecode();
105
+ if (!canDecode) {
106
+ throw new Error(`Cannot decode audio track with codec: ${track.codec}`);
107
+ }
108
+
109
+ // Create sinks with matching sample rate
110
+ // Track is set and used through iterators and sinks
111
+ this.bufferSink = new AudioBufferSink(track);
112
+ this.sampleSink = new AudioSampleSink(track);
113
+
114
+ // Ready for playback again after successful initialization
115
+ this.disposed = false;
116
+ }
117
+
118
+ async play(fromTime: number = this.pauseTime): Promise<void> {
119
+ if (this.playing || !this.bufferSink) return;
120
+
121
+ // Resume audio context if suspended
122
+ if (this.audioContext.state === 'suspended') {
123
+ await this.audioContext.resume();
124
+ }
125
+
126
+ this.playbackId++;
127
+ const currentPlaybackId = this.playbackId;
128
+
129
+ this.playing = true;
130
+ this.startContextTime = this.audioContext.currentTime;
131
+ this.startMediaTime = fromTime;
132
+ this.pauseTime = fromTime;
133
+
134
+ // Start iterator from current position
135
+ this.bufferIterator = this.bufferSink.buffers(fromTime);
136
+
137
+ // Start playing audio buffers
138
+ this.scheduleAudioBuffers(currentPlaybackId);
139
+ }
140
+
141
+ private async scheduleAudioBuffers(playbackId: number): Promise<void> {
142
+ const iterator = this.bufferIterator;
143
+ if (!iterator || !this.gainNode) return;
144
+
145
+ try {
146
+ for await (const { buffer, timestamp } of iterator) {
147
+ if (playbackId !== this.playbackId || this.disposed || !this.playing) {
148
+ break;
149
+ }
150
+
151
+ const node = this.audioContext.createBufferSource();
152
+ node.buffer = buffer;
153
+ node.connect(this.gainNode);
154
+ node.playbackRate.value = this.playbackRate;
155
+ node.playbackRate.setValueAtTime(this.playbackRate, this.audioContext.currentTime);
156
+
157
+ const scheduledContextTime = this.startContextTime + (timestamp - this.startMediaTime) / this.playbackRate;
158
+
159
+ if (scheduledContextTime >= this.audioContext.currentTime) {
160
+ node.start(scheduledContextTime);
161
+ } else {
162
+ const elapsedMedia = Math.max(0, (this.audioContext.currentTime - scheduledContextTime) * this.playbackRate);
163
+ if (elapsedMedia < buffer.duration) {
164
+ node.start(this.audioContext.currentTime, elapsedMedia);
165
+ } else {
166
+ continue;
167
+ }
168
+ }
169
+
170
+ this.queuedNodes.add(node);
171
+ node.onended = () => {
172
+ this.queuedNodes.delete(node);
173
+ };
174
+
175
+ // Throttle if we're too far ahead
176
+ if (timestamp - this.getCurrentTime() >= 1) {
177
+ await this.waitForCatchup(timestamp);
178
+ }
179
+ }
180
+ } catch {
181
+ // Iterator was closed or disposed during scheduling
182
+ }
183
+ }
184
+
185
+ private async waitForCatchup(targetTime: number): Promise<void> {
186
+ return new Promise((resolve) => {
187
+ const checkInterval = setInterval(() => {
188
+ if (targetTime - this.getCurrentTime() < 1 || !this.playing) {
189
+ clearInterval(checkInterval);
190
+ resolve();
191
+ }
192
+ }, 100);
193
+ });
194
+ }
195
+
196
+ pause(): void {
197
+ if (!this.playing) return;
198
+
199
+ const currentTime = this.getCurrentTime();
200
+ this.playing = false;
201
+ this.pauseTime = currentTime;
202
+
203
+ // Stop all queued audio nodes
204
+ this.stopQueuedNodes();
205
+
206
+ // Stop iterator
207
+ if (this.bufferIterator) {
208
+ // fire-and-forget cleanup; do not await in dispose path
209
+ void this.bufferIterator.return();
210
+ this.bufferIterator = null;
211
+ }
212
+ }
213
+
214
+ stop(): void {
215
+ this.pause();
216
+ this.pauseTime = 0;
217
+ this.startContextTime = 0;
218
+ this.startMediaTime = 0;
219
+ }
220
+
221
+ async seek(timestamp: number): Promise<void> {
222
+ const wasPlaying = this.playing;
223
+
224
+ if (wasPlaying) {
225
+ this.pause();
226
+ }
227
+
228
+ this.pauseTime = timestamp;
229
+
230
+ if (wasPlaying) {
231
+ await this.play(timestamp);
232
+ }
233
+ }
234
+
235
+ getCurrentTime(): number {
236
+ if (this.playing) {
237
+ const elapsedContext = this.audioContext.currentTime - this.startContextTime;
238
+ return this.startMediaTime + elapsedContext * this.playbackRate;
239
+ }
240
+ return this.pauseTime;
241
+ }
242
+
243
+ setVolume(volume: number): void {
244
+ this.volume = Math.max(0, Math.min(1, volume));
245
+ this.updateGain();
246
+ }
247
+
248
+ setMuted(muted: boolean): void {
249
+ this.muted = muted;
250
+ this.updateGain();
251
+ }
252
+
253
+ private updateGain(): void {
254
+ if (!this.gainNode) return;
255
+
256
+ const actualVolume = this.muted ? 0 : this.volume;
257
+ // Use quadratic curve for more natural volume control
258
+ this.gainNode.gain.value = actualVolume * actualVolume;
259
+ }
260
+
261
+ getVolume(): number {
262
+ return this.volume;
263
+ }
264
+
265
+ isMuted(): boolean {
266
+ return this.muted;
267
+ }
268
+
269
+ isPlaying(): boolean {
270
+ return this.playing;
271
+ }
272
+
273
+ setPlaybackRate(rate: number): void {
274
+ const clampedRate = Math.max(0.25, Math.min(4, rate));
275
+ if (this.playbackRate === clampedRate) {
276
+ return;
277
+ }
278
+
279
+ const wasPlaying = this.playing;
280
+ const currentTime = this.getCurrentTime();
281
+
282
+ this.playbackRate = clampedRate;
283
+
284
+ if (wasPlaying) {
285
+ this.pause();
286
+ this.pauseTime = currentTime;
287
+ void this.play(currentTime);
288
+ }
289
+ }
290
+
291
+ getAudioContext(): AudioContext {
292
+ return this.audioContext;
293
+ }
294
+
295
+ async getBufferAt(timestamp: number): Promise<WrappedAudioBuffer | null> {
296
+ if (!this.bufferSink) return null;
297
+ return this.bufferSink.getBuffer(timestamp);
298
+ }
299
+
300
+ async getSampleAt(timestamp: number): Promise<AudioSample | null> {
301
+ if (!this.sampleSink) return null;
302
+ return this.sampleSink.getSample(timestamp);
303
+ }
304
+
305
+ private stopQueuedNodes(): void {
306
+ for (const node of this.queuedNodes) {
307
+ try {
308
+ node.stop();
309
+ } catch {
310
+ // Node might have already ended
311
+ }
312
+ }
313
+ this.queuedNodes.clear();
314
+ }
315
+
316
+ /**
317
+ * Clears iterators to stop any in-flight async operations.
318
+ * Called before disposing the input to prevent accessing disposed resources.
319
+ */
320
+ async clearIterators(): Promise<void> {
321
+ this.playbackId++;
322
+ this.stop();
323
+
324
+ if (this.bufferIterator) {
325
+ try {
326
+ await this.bufferIterator.return();
327
+ } catch {
328
+ // Iterator may already be closed
329
+ }
330
+ this.bufferIterator = null;
331
+ }
332
+ }
333
+
334
+ dispose(): void {
335
+ this.disposed = true;
336
+ this.playbackId++;
337
+ this.stop();
338
+
339
+ if (this.bufferIterator) {
340
+ void this.bufferIterator.return();
341
+ this.bufferIterator = null;
342
+ }
343
+
344
+ this.bufferSink = null;
345
+ this.sampleSink = null;
346
+ // Track reference cleared through dispose of iterators
347
+ }
348
+
349
+ destroy(): void {
350
+ this.dispose();
351
+
352
+ if (this.gainNode) {
353
+ this.gainNode.disconnect();
354
+ this.gainNode = null;
355
+ }
356
+
357
+ if (this.audioContext.state !== 'closed') {
358
+ this.audioContext.close();
359
+ }
360
+ }
361
+ }