@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
|
@@ -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
|
+
}
|