@idealyst/audio 1.2.48
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/package.json +69 -0
- package/src/constants.ts +161 -0
- package/src/context/AudioContext.native.ts +84 -0
- package/src/context/AudioContext.web.ts +97 -0
- package/src/context/index.native.ts +1 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAudio.ts +129 -0
- package/src/hooks/usePlayer.ts +247 -0
- package/src/hooks/useRecorder.ts +176 -0
- package/src/index.native.ts +114 -0
- package/src/index.ts +114 -0
- package/src/index.web.ts +8 -0
- package/src/playback/Player.native.ts +517 -0
- package/src/playback/Player.web.ts +518 -0
- package/src/playback/index.native.ts +1 -0
- package/src/playback/index.ts +1 -0
- package/src/recording/Recorder.native.ts +330 -0
- package/src/recording/Recorder.web.ts +399 -0
- package/src/recording/index.native.ts +1 -0
- package/src/recording/index.ts +1 -0
- package/src/session/AudioSession.native.ts +204 -0
- package/src/session/AudioSession.web.ts +69 -0
- package/src/session/index.native.ts +5 -0
- package/src/session/index.ts +1 -0
- package/src/types.ts +470 -0
- package/src/utils.ts +379 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Player
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Audio API for playback on web.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
IPlayer,
|
|
9
|
+
PlayerStatus,
|
|
10
|
+
PlayerState,
|
|
11
|
+
AudioConfig,
|
|
12
|
+
PlayerStateCallback,
|
|
13
|
+
PlayerPositionCallback,
|
|
14
|
+
PlayerBufferCallback,
|
|
15
|
+
PlayerEndedCallback,
|
|
16
|
+
IAudioContext,
|
|
17
|
+
} from '../types';
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_PLAYER_STATUS,
|
|
20
|
+
DEFAULT_POSITION_UPDATE_INTERVAL,
|
|
21
|
+
} from '../constants';
|
|
22
|
+
import {
|
|
23
|
+
createAudioError,
|
|
24
|
+
pcmToFloat32,
|
|
25
|
+
resampleLinear,
|
|
26
|
+
clamp,
|
|
27
|
+
samplesToDuration,
|
|
28
|
+
} from '../utils';
|
|
29
|
+
|
|
30
|
+
export class WebPlayer implements IPlayer {
|
|
31
|
+
private audioContext: IAudioContext;
|
|
32
|
+
private gainNode: GainNode | null = null;
|
|
33
|
+
private sourceNode: AudioBufferSourceNode | null = null;
|
|
34
|
+
|
|
35
|
+
private _status: PlayerStatus = { ...DEFAULT_PLAYER_STATUS };
|
|
36
|
+
private pcmConfig: AudioConfig | null = null;
|
|
37
|
+
private audioBuffer: AudioBuffer | null = null;
|
|
38
|
+
private startTime: number = 0;
|
|
39
|
+
private pausePosition: number = 0;
|
|
40
|
+
|
|
41
|
+
// PCM streaming
|
|
42
|
+
private pcmBuffer: Float32Array[] = [];
|
|
43
|
+
private isStreaming: boolean = false;
|
|
44
|
+
private streamScheduleInterval: ReturnType<typeof setInterval> | null = null;
|
|
45
|
+
private nextScheduleTime: number = 0;
|
|
46
|
+
|
|
47
|
+
// Position tracking
|
|
48
|
+
private positionIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
49
|
+
|
|
50
|
+
// Callbacks
|
|
51
|
+
private stateCallbacks: Set<PlayerStateCallback> = new Set();
|
|
52
|
+
private positionCallbacks: Map<PlayerPositionCallback, number> = new Map();
|
|
53
|
+
private bufferCallbacks: Set<PlayerBufferCallback> = new Set();
|
|
54
|
+
private endedCallbacks: Set<PlayerEndedCallback> = new Set();
|
|
55
|
+
|
|
56
|
+
constructor(audioContext: IAudioContext) {
|
|
57
|
+
this.audioContext = audioContext;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get status(): PlayerStatus {
|
|
61
|
+
return { ...this._status };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async loadFile(uri: string): Promise<void> {
|
|
65
|
+
this.cleanup();
|
|
66
|
+
this.updateState('loading');
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (!this.audioContext.isInitialized) {
|
|
70
|
+
await this.audioContext.initialize();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
74
|
+
if (!ctx) {
|
|
75
|
+
throw createAudioError('INITIALIZATION_FAILED', 'AudioContext not available');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await this.ensureGainNode(ctx);
|
|
79
|
+
|
|
80
|
+
// Fetch the audio file
|
|
81
|
+
const response = await fetch(uri);
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw createAudioError('SOURCE_NOT_FOUND', `Failed to fetch audio: ${response.status}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
87
|
+
this.audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
|
88
|
+
|
|
89
|
+
this._status.duration = this.audioBuffer.duration * 1000;
|
|
90
|
+
this.updateState('ready');
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
this.handleError(error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
unload(): void {
|
|
98
|
+
this.cleanup();
|
|
99
|
+
this._status = { ...DEFAULT_PLAYER_STATUS };
|
|
100
|
+
this.pcmConfig = null;
|
|
101
|
+
this.audioBuffer = null;
|
|
102
|
+
this.pcmBuffer = [];
|
|
103
|
+
this.notifyStateChange();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async loadPCMStream(config: AudioConfig): Promise<void> {
|
|
107
|
+
this.cleanup();
|
|
108
|
+
this.updateState('loading');
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (!this.audioContext.isInitialized) {
|
|
112
|
+
await this.audioContext.initialize();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
116
|
+
if (!ctx) {
|
|
117
|
+
throw createAudioError('INITIALIZATION_FAILED', 'AudioContext not available');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await this.ensureGainNode(ctx);
|
|
121
|
+
|
|
122
|
+
this.pcmConfig = { ...config };
|
|
123
|
+
this._status.duration = 0;
|
|
124
|
+
this.pcmBuffer = [];
|
|
125
|
+
this.isStreaming = false;
|
|
126
|
+
this.updateState('ready');
|
|
127
|
+
} catch (error: any) {
|
|
128
|
+
this.handleError(error);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
feedPCMData(data: ArrayBuffer | Int16Array): void {
|
|
134
|
+
if (!this.pcmConfig) {
|
|
135
|
+
console.warn('Cannot feed PCM data: stream not loaded');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
140
|
+
if (!ctx) return;
|
|
141
|
+
|
|
142
|
+
// Convert to Float32
|
|
143
|
+
let float32Samples: Float32Array;
|
|
144
|
+
if (data instanceof Int16Array) {
|
|
145
|
+
float32Samples = pcmToFloat32(data, 16);
|
|
146
|
+
} else {
|
|
147
|
+
float32Samples = pcmToFloat32(data, this.pcmConfig.bitDepth);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resample if needed
|
|
151
|
+
if (this.pcmConfig.sampleRate !== ctx.sampleRate) {
|
|
152
|
+
float32Samples = resampleLinear(
|
|
153
|
+
float32Samples,
|
|
154
|
+
this.pcmConfig.sampleRate,
|
|
155
|
+
ctx.sampleRate
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add to buffer
|
|
160
|
+
this.pcmBuffer.push(float32Samples);
|
|
161
|
+
|
|
162
|
+
// Update buffered status
|
|
163
|
+
const totalSamples = this.pcmBuffer.reduce((sum, arr) => sum + arr.length, 0);
|
|
164
|
+
const bufferedMs = samplesToDuration(totalSamples, ctx.sampleRate);
|
|
165
|
+
this._status.buffered = bufferedMs;
|
|
166
|
+
this.notifyBufferChange(bufferedMs);
|
|
167
|
+
|
|
168
|
+
// If playing, schedule the new samples
|
|
169
|
+
if (this.isStreaming && this._status.state === 'playing') {
|
|
170
|
+
this.scheduleNextBuffer();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async flush(): Promise<void> {
|
|
175
|
+
if (this.isStreaming && this._status.state === 'playing') {
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const checkBuffer = () => {
|
|
178
|
+
const totalSamples = this.pcmBuffer.reduce((sum, arr) => sum + arr.length, 0);
|
|
179
|
+
if (totalSamples === 0 || this._status.state !== 'playing') {
|
|
180
|
+
resolve();
|
|
181
|
+
} else {
|
|
182
|
+
setTimeout(checkBuffer, 50);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
checkBuffer();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async play(): Promise<void> {
|
|
191
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
192
|
+
if (!ctx) {
|
|
193
|
+
throw createAudioError('INVALID_STATE', 'No audio source loaded');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.pcmConfig) {
|
|
197
|
+
// PCM streaming playback
|
|
198
|
+
this.isStreaming = true;
|
|
199
|
+
this.nextScheduleTime = ctx.currentTime;
|
|
200
|
+
this.startTime = ctx.currentTime;
|
|
201
|
+
this.startStreamScheduler();
|
|
202
|
+
this.startPositionTracking();
|
|
203
|
+
this.updateState('playing');
|
|
204
|
+
} else if (this.audioBuffer) {
|
|
205
|
+
// File playback
|
|
206
|
+
this.stopSourceNode();
|
|
207
|
+
|
|
208
|
+
this.sourceNode = ctx.createBufferSource();
|
|
209
|
+
this.sourceNode.buffer = this.audioBuffer;
|
|
210
|
+
this.sourceNode.connect(this.gainNode!);
|
|
211
|
+
|
|
212
|
+
this.sourceNode.onended = () => {
|
|
213
|
+
if (this._status.state === 'playing') {
|
|
214
|
+
this.updateState('stopped');
|
|
215
|
+
this._status.position = 0;
|
|
216
|
+
this.pausePosition = 0;
|
|
217
|
+
this.stopPositionTracking();
|
|
218
|
+
this.notifyEnded();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const offset = this.pausePosition / 1000;
|
|
223
|
+
this.sourceNode.start(0, offset);
|
|
224
|
+
this.startTime = ctx.currentTime - offset;
|
|
225
|
+
this.startPositionTracking();
|
|
226
|
+
this.updateState('playing');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pause(): void {
|
|
231
|
+
if (this._status.state !== 'playing') return;
|
|
232
|
+
|
|
233
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
234
|
+
|
|
235
|
+
if (this.pcmConfig) {
|
|
236
|
+
this.isStreaming = false;
|
|
237
|
+
this.stopStreamScheduler();
|
|
238
|
+
} else if (this.sourceNode && ctx) {
|
|
239
|
+
this.pausePosition = (ctx.currentTime - this.startTime) * 1000;
|
|
240
|
+
this.stopSourceNode();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.stopPositionTracking();
|
|
244
|
+
this.updateState('paused');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
stop(): void {
|
|
248
|
+
if (this._status.state === 'idle') return;
|
|
249
|
+
|
|
250
|
+
if (this.pcmConfig) {
|
|
251
|
+
this.isStreaming = false;
|
|
252
|
+
this.stopStreamScheduler();
|
|
253
|
+
this.pcmBuffer = [];
|
|
254
|
+
} else if (this.sourceNode) {
|
|
255
|
+
this.stopSourceNode();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.stopPositionTracking();
|
|
259
|
+
this._status.position = 0;
|
|
260
|
+
this.pausePosition = 0;
|
|
261
|
+
this._status.buffered = 0;
|
|
262
|
+
this.updateState('stopped');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async seek(positionMs: number): Promise<void> {
|
|
266
|
+
if (this.pcmConfig) {
|
|
267
|
+
throw createAudioError('INVALID_STATE', 'Cannot seek in PCM stream');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!this.audioBuffer) {
|
|
271
|
+
throw createAudioError('INVALID_STATE', 'No audio loaded');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const wasPlaying = this._status.state === 'playing';
|
|
275
|
+
const clampedPosition = clamp(positionMs, 0, this._status.duration);
|
|
276
|
+
|
|
277
|
+
if (wasPlaying) {
|
|
278
|
+
this.stopSourceNode();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.pausePosition = clampedPosition;
|
|
282
|
+
this._status.position = clampedPosition;
|
|
283
|
+
this.notifyStateChange();
|
|
284
|
+
|
|
285
|
+
if (wasPlaying) {
|
|
286
|
+
await this.play();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
setVolume(volume: number): void {
|
|
291
|
+
const clampedVolume = clamp(volume, 0, 1);
|
|
292
|
+
this._status.volume = clampedVolume;
|
|
293
|
+
|
|
294
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
295
|
+
if (this.gainNode && ctx) {
|
|
296
|
+
this.gainNode.gain.setValueAtTime(
|
|
297
|
+
this._status.muted ? 0 : clampedVolume,
|
|
298
|
+
ctx.currentTime
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.notifyStateChange();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
setMuted(muted: boolean): void {
|
|
306
|
+
this._status.muted = muted;
|
|
307
|
+
|
|
308
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
309
|
+
if (this.gainNode && ctx) {
|
|
310
|
+
this.gainNode.gain.setValueAtTime(
|
|
311
|
+
muted ? 0 : this._status.volume,
|
|
312
|
+
ctx.currentTime
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.notifyStateChange();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
onStateChange(callback: PlayerStateCallback): () => void {
|
|
320
|
+
this.stateCallbacks.add(callback);
|
|
321
|
+
return () => this.stateCallbacks.delete(callback);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onPosition(callback: PlayerPositionCallback, intervalMs = DEFAULT_POSITION_UPDATE_INTERVAL): () => void {
|
|
325
|
+
this.positionCallbacks.set(callback, intervalMs);
|
|
326
|
+
return () => this.positionCallbacks.delete(callback);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
onBufferChange(callback: PlayerBufferCallback): () => void {
|
|
330
|
+
this.bufferCallbacks.add(callback);
|
|
331
|
+
return () => this.bufferCallbacks.delete(callback);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
onEnded(callback: PlayerEndedCallback): () => void {
|
|
335
|
+
this.endedCallbacks.add(callback);
|
|
336
|
+
return () => this.endedCallbacks.delete(callback);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
dispose(): void {
|
|
340
|
+
this.cleanup();
|
|
341
|
+
this.stateCallbacks.clear();
|
|
342
|
+
this.positionCallbacks.clear();
|
|
343
|
+
this.bufferCallbacks.clear();
|
|
344
|
+
this.endedCallbacks.clear();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Private methods
|
|
348
|
+
|
|
349
|
+
private async ensureGainNode(ctx: AudioContext): Promise<void> {
|
|
350
|
+
if (!this.gainNode) {
|
|
351
|
+
this.gainNode = ctx.createGain();
|
|
352
|
+
this.gainNode.gain.setValueAtTime(this._status.volume, ctx.currentTime);
|
|
353
|
+
this.gainNode.connect(ctx.destination);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private scheduleNextBuffer(): void {
|
|
358
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
359
|
+
if (!ctx || !this.gainNode || this.pcmBuffer.length === 0) return;
|
|
360
|
+
|
|
361
|
+
const chunk = this.pcmBuffer.shift();
|
|
362
|
+
if (!chunk) return;
|
|
363
|
+
|
|
364
|
+
const audioBuffer = ctx.createBuffer(1, chunk.length, ctx.sampleRate);
|
|
365
|
+
audioBuffer.getChannelData(0).set(chunk);
|
|
366
|
+
|
|
367
|
+
const source = ctx.createBufferSource();
|
|
368
|
+
source.buffer = audioBuffer;
|
|
369
|
+
source.connect(this.gainNode);
|
|
370
|
+
|
|
371
|
+
const scheduleTime = Math.max(this.nextScheduleTime, ctx.currentTime);
|
|
372
|
+
source.start(scheduleTime);
|
|
373
|
+
|
|
374
|
+
this.nextScheduleTime = scheduleTime + audioBuffer.duration;
|
|
375
|
+
|
|
376
|
+
// Update buffered status
|
|
377
|
+
const totalSamples = this.pcmBuffer.reduce((sum, arr) => sum + arr.length, 0);
|
|
378
|
+
const bufferedMs = samplesToDuration(totalSamples, ctx.sampleRate);
|
|
379
|
+
this._status.buffered = bufferedMs;
|
|
380
|
+
this.notifyBufferChange(bufferedMs);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private startStreamScheduler(): void {
|
|
384
|
+
this.stopStreamScheduler();
|
|
385
|
+
|
|
386
|
+
this.streamScheduleInterval = setInterval(() => {
|
|
387
|
+
if (this.isStreaming && this.pcmBuffer.length > 0) {
|
|
388
|
+
this.scheduleNextBuffer();
|
|
389
|
+
}
|
|
390
|
+
}, 50);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private stopStreamScheduler(): void {
|
|
394
|
+
if (this.streamScheduleInterval) {
|
|
395
|
+
clearInterval(this.streamScheduleInterval);
|
|
396
|
+
this.streamScheduleInterval = null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private stopSourceNode(): void {
|
|
401
|
+
if (this.sourceNode) {
|
|
402
|
+
try {
|
|
403
|
+
this.sourceNode.stop();
|
|
404
|
+
this.sourceNode.disconnect();
|
|
405
|
+
} catch {
|
|
406
|
+
// Ignore errors
|
|
407
|
+
}
|
|
408
|
+
this.sourceNode = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private cleanup(): void {
|
|
413
|
+
this.stopPositionTracking();
|
|
414
|
+
this.stopStreamScheduler();
|
|
415
|
+
this.stopSourceNode();
|
|
416
|
+
this.isStreaming = false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private updateState(state: PlayerState): void {
|
|
420
|
+
this._status.state = state;
|
|
421
|
+
this._status.isPlaying = state === 'playing';
|
|
422
|
+
this._status.isPaused = state === 'paused';
|
|
423
|
+
this.notifyStateChange();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private notifyStateChange(): void {
|
|
427
|
+
const status = this.status;
|
|
428
|
+
this.stateCallbacks.forEach((callback) => {
|
|
429
|
+
try {
|
|
430
|
+
callback(status);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
console.error('Error in state callback:', e);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private notifyBufferChange(buffered: number): void {
|
|
438
|
+
this.bufferCallbacks.forEach((callback) => {
|
|
439
|
+
try {
|
|
440
|
+
callback(buffered);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error('Error in buffer callback:', e);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private notifyEnded(): void {
|
|
448
|
+
this.endedCallbacks.forEach((callback) => {
|
|
449
|
+
try {
|
|
450
|
+
callback();
|
|
451
|
+
} catch (e) {
|
|
452
|
+
console.error('Error in ended callback:', e);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private startPositionTracking(): void {
|
|
458
|
+
this.stopPositionTracking();
|
|
459
|
+
|
|
460
|
+
let minInterval = DEFAULT_POSITION_UPDATE_INTERVAL;
|
|
461
|
+
this.positionCallbacks.forEach((interval) => {
|
|
462
|
+
if (interval < minInterval) minInterval = interval;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
this.positionIntervalId = setInterval(() => {
|
|
466
|
+
const ctx = this.audioContext.getContext() as AudioContext;
|
|
467
|
+
if (this._status.state !== 'playing' || !ctx) return;
|
|
468
|
+
|
|
469
|
+
const position = (ctx.currentTime - this.startTime) * 1000;
|
|
470
|
+
this._status.position = position;
|
|
471
|
+
|
|
472
|
+
// Check if file playback ended
|
|
473
|
+
if (
|
|
474
|
+
!this.pcmConfig &&
|
|
475
|
+
this._status.duration > 0 &&
|
|
476
|
+
position >= this._status.duration
|
|
477
|
+
) {
|
|
478
|
+
this.updateState('stopped');
|
|
479
|
+
this._status.position = 0;
|
|
480
|
+
this.pausePosition = 0;
|
|
481
|
+
this.stopPositionTracking();
|
|
482
|
+
this.notifyEnded();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.positionCallbacks.forEach((_, callback) => {
|
|
487
|
+
try {
|
|
488
|
+
callback(position);
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.error('Error in position callback:', e);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}, minInterval);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private stopPositionTracking(): void {
|
|
497
|
+
if (this.positionIntervalId) {
|
|
498
|
+
clearInterval(this.positionIntervalId);
|
|
499
|
+
this.positionIntervalId = null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private handleError(error: any): void {
|
|
504
|
+
const audioError = createAudioError(
|
|
505
|
+
error.code || 'UNKNOWN',
|
|
506
|
+
error.message || String(error),
|
|
507
|
+
error instanceof Error ? error : undefined
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
this._status.state = 'error';
|
|
511
|
+
this._status.error = audioError;
|
|
512
|
+
this.notifyStateChange();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function createPlayer(audioContext: IAudioContext): IPlayer {
|
|
517
|
+
return new WebPlayer(audioContext);
|
|
518
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NativePlayer, createPlayer } from './Player.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebPlayer, createPlayer } from './Player.web';
|