audioq 2.0.0
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/LICENSE +21 -0
- package/README.md +486 -0
- package/dist/core.d.ts +129 -0
- package/dist/core.js +591 -0
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +441 -0
- package/dist/events.d.ts +81 -0
- package/dist/events.js +217 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +119 -0
- package/dist/info.d.ts +224 -0
- package/dist/info.js +529 -0
- package/dist/pause.d.ts +170 -0
- package/dist/pause.js +467 -0
- package/dist/queue-manipulation.d.ts +104 -0
- package/dist/queue-manipulation.js +319 -0
- package/dist/types.d.ts +382 -0
- package/dist/types.js +55 -0
- package/dist/utils.d.ts +83 -0
- package/dist/utils.js +215 -0
- package/dist/volume.d.ts +162 -0
- package/dist/volume.js +644 -0
- package/dist/web-audio.d.ts +156 -0
- package/dist/web-audio.js +327 -0
- package/package.json +63 -0
- package/src/core.ts +698 -0
- package/src/errors.ts +467 -0
- package/src/events.ts +252 -0
- package/src/index.ts +162 -0
- package/src/info.ts +590 -0
- package/src/pause.ts +523 -0
- package/src/queue-manipulation.ts +378 -0
- package/src/types.ts +415 -0
- package/src/utils.ts +235 -0
- package/src/volume.ts +735 -0
- package/src/web-audio.ts +331 -0
package/src/web-audio.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Web Audio API support for enhanced volume control on iOS and other platforms
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { WebAudioConfig, WebAudioSupport, WebAudioNodeSet } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Global Web Audio API configuration
|
|
9
|
+
*/
|
|
10
|
+
let webAudioConfig: WebAudioConfig = {
|
|
11
|
+
autoDetectIOS: true,
|
|
12
|
+
enabled: true,
|
|
13
|
+
forceWebAudio: false
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detects if the current device is iOS
|
|
18
|
+
* @returns True if the device is iOS, false otherwise
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* if (isIOSDevice()) {
|
|
22
|
+
* console.log('Running on iOS device');
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const isIOSDevice = (): boolean => {
|
|
27
|
+
if (typeof navigator === 'undefined') return false;
|
|
28
|
+
|
|
29
|
+
// Modern approach using User-Agent Client Hints API
|
|
30
|
+
const navWithUA = navigator as unknown as { userAgentData?: { platform: string } };
|
|
31
|
+
if ('userAgentData' in navigator && navWithUA.userAgentData) {
|
|
32
|
+
return navWithUA.userAgentData.platform === 'iOS';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fallback to userAgent string parsing
|
|
36
|
+
const userAgent = navigator.userAgent || '';
|
|
37
|
+
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
|
38
|
+
|
|
39
|
+
// Additional check for modern iPads that report as Mac
|
|
40
|
+
const isMacWithTouch =
|
|
41
|
+
/Macintosh/.test(userAgent) && 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 1;
|
|
42
|
+
|
|
43
|
+
return isIOS || isMacWithTouch;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if Web Audio API is available in the current environment
|
|
48
|
+
* @returns True if Web Audio API is supported, false otherwise
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* if (isWebAudioSupported()) {
|
|
52
|
+
* console.log('Web Audio API is available');
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const isWebAudioSupported = (): boolean => {
|
|
57
|
+
if (typeof window === 'undefined') {
|
|
58
|
+
// In Node.js environment (tests), check if Web Audio API globals are available
|
|
59
|
+
const globalThis = global as unknown as {
|
|
60
|
+
AudioContext?: unknown;
|
|
61
|
+
webkitAudioContext?: unknown;
|
|
62
|
+
};
|
|
63
|
+
return (
|
|
64
|
+
typeof globalThis.AudioContext !== 'undefined' ||
|
|
65
|
+
typeof globalThis.webkitAudioContext !== 'undefined'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const windowWithWebkit = window as unknown as { webkitAudioContext?: unknown };
|
|
69
|
+
return (
|
|
70
|
+
typeof AudioContext !== 'undefined' ||
|
|
71
|
+
typeof windowWithWebkit.webkitAudioContext !== 'undefined'
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determines if Web Audio API should be used based on configuration and device detection
|
|
77
|
+
* @returns True if Web Audio API should be used, false otherwise
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* if (shouldUseWebAudio()) {
|
|
81
|
+
* // Use Web Audio API for volume control
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export const shouldUseWebAudio = (): boolean => {
|
|
86
|
+
if (!webAudioConfig.enabled) return false;
|
|
87
|
+
if (!isWebAudioSupported()) return false;
|
|
88
|
+
if (webAudioConfig.forceWebAudio) return true;
|
|
89
|
+
if (webAudioConfig.autoDetectIOS && isIOSDevice()) return true;
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gets information about Web Audio API support and usage
|
|
95
|
+
* @returns Object containing Web Audio API support information
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const support = getWebAudioSupport();
|
|
99
|
+
* console.log(`Using Web Audio: ${support.usingWebAudio}`);
|
|
100
|
+
* console.log(`Reason: ${support.reason}`);
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export const getWebAudioSupport = (): WebAudioSupport => {
|
|
104
|
+
const available = isWebAudioSupported();
|
|
105
|
+
const isIOS = isIOSDevice();
|
|
106
|
+
const usingWebAudio = shouldUseWebAudio();
|
|
107
|
+
|
|
108
|
+
let reason = '';
|
|
109
|
+
if (!webAudioConfig.enabled) {
|
|
110
|
+
reason = 'Web Audio API disabled in configuration';
|
|
111
|
+
} else if (!available) {
|
|
112
|
+
reason = 'Web Audio API not supported in this environment';
|
|
113
|
+
} else if (webAudioConfig.forceWebAudio) {
|
|
114
|
+
reason = 'Web Audio API forced via configuration';
|
|
115
|
+
} else if (isIOS && webAudioConfig.autoDetectIOS) {
|
|
116
|
+
reason = 'iOS device detected - using Web Audio API for volume control';
|
|
117
|
+
} else {
|
|
118
|
+
reason = 'Using standard HTMLAudioElement volume control';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
available,
|
|
123
|
+
isIOS,
|
|
124
|
+
reason,
|
|
125
|
+
usingWebAudio
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Configures Web Audio API usage
|
|
131
|
+
* @param config - Configuration options for Web Audio API
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* // Force Web Audio API usage on all devices
|
|
135
|
+
* setWebAudioConfig({ forceWebAudio: true });
|
|
136
|
+
*
|
|
137
|
+
* // Disable Web Audio API entirely
|
|
138
|
+
* setWebAudioConfig({ enabled: false });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export const setWebAudioConfig = (config: Partial<WebAudioConfig>): void => {
|
|
142
|
+
webAudioConfig = { ...webAudioConfig, ...config };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Gets the current Web Audio API configuration
|
|
147
|
+
* @returns Current Web Audio API configuration
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const config = getWebAudioConfig();
|
|
151
|
+
* console.log(`Web Audio enabled: ${config.enabled}`);
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export const getWebAudioConfig = (): WebAudioConfig => {
|
|
155
|
+
return { ...webAudioConfig };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates or gets an AudioContext for Web Audio API operations
|
|
160
|
+
* @returns AudioContext instance or null if not supported
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const context = getAudioContext();
|
|
164
|
+
* if (context) {
|
|
165
|
+
* console.log('Audio context created successfully');
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export const getAudioContext = (): AudioContext | null => {
|
|
170
|
+
if (!isWebAudioSupported()) return null;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// In Node.js environment (tests), return null to allow mocking
|
|
174
|
+
if (typeof window === 'undefined') {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Use existing AudioContext or create new one
|
|
179
|
+
const windowWithWebkit = window as unknown as { webkitAudioContext?: typeof AudioContext };
|
|
180
|
+
const AudioContextClass = window.AudioContext || windowWithWebkit.webkitAudioContext;
|
|
181
|
+
return new AudioContextClass();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
// eslint-disable-next-line no-console
|
|
184
|
+
console.error('Failed to create AudioContext:', error);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates Web Audio API nodes for an audio element
|
|
191
|
+
* @param audioElement - The HTML audio element to create nodes for
|
|
192
|
+
* @param audioContext - The AudioContext to use
|
|
193
|
+
* @returns Web Audio API node set or null if creation fails
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const audio = new Audio('song.mp3');
|
|
197
|
+
* const context = getAudioContext();
|
|
198
|
+
* if (context) {
|
|
199
|
+
* const nodes = createWebAudioNodes(audio, context);
|
|
200
|
+
* if (nodes) {
|
|
201
|
+
* nodes.gainNode.gain.value = 0.5; // Set volume to 50%
|
|
202
|
+
* }
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export const createWebAudioNodes = (
|
|
207
|
+
audioElement: HTMLAudioElement,
|
|
208
|
+
audioContext: AudioContext
|
|
209
|
+
): WebAudioNodeSet | null => {
|
|
210
|
+
try {
|
|
211
|
+
// Create media element source node
|
|
212
|
+
const sourceNode = audioContext.createMediaElementSource(audioElement);
|
|
213
|
+
|
|
214
|
+
// Create gain node for volume control
|
|
215
|
+
const gainNode = audioContext.createGain();
|
|
216
|
+
|
|
217
|
+
// Connect source to gain node
|
|
218
|
+
sourceNode.connect(gainNode);
|
|
219
|
+
|
|
220
|
+
// Connect gain node to destination (speakers)
|
|
221
|
+
gainNode.connect(audioContext.destination);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
gainNode,
|
|
225
|
+
sourceNode
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// eslint-disable-next-line no-console
|
|
229
|
+
console.error('Failed to create Web Audio nodes:', error);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Sets volume using Web Audio API gain node
|
|
236
|
+
* @param gainNode - The gain node to set volume on
|
|
237
|
+
* @param volume - Volume level (0-1)
|
|
238
|
+
* @param transitionDuration - Optional transition duration in milliseconds
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const nodes = createWebAudioNodes(audio, context);
|
|
242
|
+
* if (nodes) {
|
|
243
|
+
* setWebAudioVolume(nodes.gainNode, 0.5); // Set to 50% volume
|
|
244
|
+
* setWebAudioVolume(nodes.gainNode, 0.2, 300); // Fade to 20% over 300ms
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export const setWebAudioVolume = (
|
|
249
|
+
gainNode: GainNode,
|
|
250
|
+
volume: number,
|
|
251
|
+
transitionDuration?: number
|
|
252
|
+
): void => {
|
|
253
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
254
|
+
const currentTime = gainNode.context.currentTime;
|
|
255
|
+
|
|
256
|
+
if (transitionDuration && transitionDuration > 0) {
|
|
257
|
+
// Smooth transition using Web Audio API's built-in scheduling
|
|
258
|
+
gainNode.gain.cancelScheduledValues(currentTime);
|
|
259
|
+
gainNode.gain.setValueAtTime(gainNode.gain.value, currentTime);
|
|
260
|
+
gainNode.gain.linearRampToValueAtTime(clampedVolume, currentTime + transitionDuration / 1000);
|
|
261
|
+
} else {
|
|
262
|
+
// Instant change
|
|
263
|
+
gainNode.gain.cancelScheduledValues(currentTime);
|
|
264
|
+
gainNode.gain.value = clampedVolume;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Gets the current volume from a Web Audio API gain node
|
|
270
|
+
* @param gainNode - The gain node to get volume from
|
|
271
|
+
* @returns Current volume level (0-1)
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* const nodes = createWebAudioNodes(audio, context);
|
|
275
|
+
* if (nodes) {
|
|
276
|
+
* const volume = getWebAudioVolume(nodes.gainNode);
|
|
277
|
+
* console.log(`Current volume: ${volume * 100}%`);
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export const getWebAudioVolume = (gainNode: GainNode): number => {
|
|
282
|
+
return gainNode.gain.value;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resumes an AudioContext if it's in suspended state (required for autoplay policy)
|
|
287
|
+
* @param audioContext - The AudioContext to resume
|
|
288
|
+
* @returns Promise that resolves when context is resumed
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* const context = getAudioContext();
|
|
292
|
+
* if (context) {
|
|
293
|
+
* await resumeAudioContext(context);
|
|
294
|
+
* }
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export const resumeAudioContext = async (audioContext: AudioContext): Promise<void> => {
|
|
298
|
+
if (audioContext.state === 'suspended') {
|
|
299
|
+
try {
|
|
300
|
+
await audioContext.resume();
|
|
301
|
+
} catch (error) {
|
|
302
|
+
// eslint-disable-next-line no-console
|
|
303
|
+
console.error('Failed to resume AudioContext:', error);
|
|
304
|
+
// Don't throw - handle gracefully and continue
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Cleans up Web Audio API nodes and connections
|
|
311
|
+
* @param nodes - The Web Audio API node set to clean up
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const nodes = createWebAudioNodes(audio, context);
|
|
315
|
+
* if (nodes) {
|
|
316
|
+
* // Use nodes...
|
|
317
|
+
* cleanupWebAudioNodes(nodes); // Clean up when done
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
export const cleanupWebAudioNodes = (nodes: WebAudioNodeSet): void => {
|
|
322
|
+
try {
|
|
323
|
+
// Disconnect all nodes
|
|
324
|
+
nodes.sourceNode.disconnect();
|
|
325
|
+
nodes.gainNode.disconnect();
|
|
326
|
+
} catch (error) {
|
|
327
|
+
// Ignore errors during cleanup
|
|
328
|
+
// eslint-disable-next-line no-console
|
|
329
|
+
console.error('Error during Web Audio cleanup:', error);
|
|
330
|
+
}
|
|
331
|
+
};
|