audio-channel-queue 1.5.0 → 1.7.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/README.md +323 -96
- package/dist/core.d.ts +27 -9
- package/dist/core.js +132 -29
- package/dist/errors.d.ts +137 -0
- package/dist/errors.js +461 -0
- package/dist/events.d.ts +23 -1
- package/dist/events.js +50 -2
- package/dist/index.d.ts +9 -19
- package/dist/index.js +45 -25
- package/dist/info.d.ts +47 -2
- package/dist/info.js +126 -7
- package/dist/pause.d.ts +87 -0
- package/dist/pause.js +186 -0
- package/dist/types.d.ts +113 -12
- package/dist/utils.d.ts +7 -1
- package/dist/utils.js +24 -4
- package/dist/volume.d.ts +98 -0
- package/dist/volume.js +305 -0
- package/package.json +4 -1
- package/src/core.ts +144 -27
- package/src/errors.ts +480 -0
- package/src/events.ts +242 -188
- package/src/index.ts +91 -66
- package/src/info.ts +386 -261
- package/src/pause.ts +190 -0
- package/src/types.ts +235 -126
- package/src/utils.ts +133 -108
- package/src/volume.ts +331 -0
package/src/core.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Core queue management functions for the audio-channel-queue package
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { ExtendedAudioQueueChannel } from './types';
|
|
5
|
+
import { ExtendedAudioQueueChannel, AudioQueueOptions } from './types';
|
|
6
6
|
import { audioChannels } from './info';
|
|
7
7
|
import { extractFileName } from './utils';
|
|
8
8
|
import {
|
|
@@ -12,39 +12,116 @@ import {
|
|
|
12
12
|
setupProgressTracking,
|
|
13
13
|
cleanupProgressTracking
|
|
14
14
|
} from './events';
|
|
15
|
+
import { applyVolumeDucking, restoreVolumeLevels } from './volume';
|
|
16
|
+
import { setupAudioErrorHandling, handleAudioError } from './errors';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Queues an audio file to a specific channel and starts playing if it's the first in queue
|
|
18
20
|
* @param audioUrl - The URL of the audio file to queue
|
|
19
21
|
* @param channelNumber - The channel number to queue the audio to (defaults to 0)
|
|
22
|
+
* @param options - Optional configuration for the audio file
|
|
20
23
|
* @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
|
|
21
24
|
* @example
|
|
22
25
|
* ```typescript
|
|
23
26
|
* await queueAudio('https://example.com/song.mp3', 0);
|
|
24
27
|
* await queueAudio('./sounds/notification.wav'); // Uses default channel 0
|
|
28
|
+
* await queueAudio('./music/loop.mp3', 1, { loop: true }); // Loop the audio
|
|
29
|
+
* await queueAudio('./urgent.wav', 0, { addToFront: true }); // Add to front of queue
|
|
25
30
|
* ```
|
|
26
31
|
*/
|
|
27
|
-
export const queueAudio = async (
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
export const queueAudio = async (
|
|
33
|
+
audioUrl: string,
|
|
34
|
+
channelNumber: number = 0,
|
|
35
|
+
options?: AudioQueueOptions
|
|
36
|
+
): Promise<void> => {
|
|
37
|
+
// Ensure the channel exists
|
|
38
|
+
while (audioChannels.length <= channelNumber) {
|
|
39
|
+
audioChannels.push({
|
|
30
40
|
audioCompleteCallbacks: new Set(),
|
|
41
|
+
audioErrorCallbacks: new Set(),
|
|
42
|
+
audioPauseCallbacks: new Set(),
|
|
43
|
+
audioResumeCallbacks: new Set(),
|
|
31
44
|
audioStartCallbacks: new Set(),
|
|
45
|
+
isPaused: false,
|
|
32
46
|
progressCallbacks: new Map(),
|
|
33
47
|
queue: [],
|
|
34
|
-
queueChangeCallbacks: new Set()
|
|
35
|
-
|
|
48
|
+
queueChangeCallbacks: new Set(),
|
|
49
|
+
volume: 1.0
|
|
50
|
+
});
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
38
54
|
const audio: HTMLAudioElement = new Audio(audioUrl);
|
|
39
|
-
audioChannels[channelNumber].queue.push(audio);
|
|
40
55
|
|
|
56
|
+
// Set up comprehensive error handling
|
|
57
|
+
setupAudioErrorHandling(audio, channelNumber, audioUrl, async (error: Error) => {
|
|
58
|
+
await handleAudioError(audio, channelNumber, audioUrl, error);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Apply options if provided
|
|
62
|
+
if (options) {
|
|
63
|
+
if (typeof options.loop === 'boolean') {
|
|
64
|
+
audio.loop = options.loop;
|
|
65
|
+
}
|
|
66
|
+
if (typeof options.volume === 'number' && !isNaN(options.volume)) {
|
|
67
|
+
const clampedVolume = Math.max(0, Math.min(1, options.volume));
|
|
68
|
+
audio.volume = clampedVolume;
|
|
69
|
+
// Set channel volume to match the audio volume
|
|
70
|
+
channel.volume = clampedVolume;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle priority option (same as addToFront for backward compatibility)
|
|
75
|
+
const shouldAddToFront = options?.addToFront || options?.priority;
|
|
76
|
+
|
|
77
|
+
// Add to queue based on priority/addToFront option
|
|
78
|
+
if (shouldAddToFront && channel.queue.length > 0) {
|
|
79
|
+
// Insert after currently playing track (at index 1)
|
|
80
|
+
channel.queue.splice(1, 0, audio);
|
|
81
|
+
} else if (shouldAddToFront) {
|
|
82
|
+
// If queue is empty, add to front
|
|
83
|
+
channel.queue.unshift(audio);
|
|
84
|
+
} else {
|
|
85
|
+
// Add to back of queue
|
|
86
|
+
channel.queue.push(audio);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Emit queue change event
|
|
41
90
|
emitQueueChange(channelNumber, audioChannels);
|
|
42
91
|
|
|
43
|
-
if
|
|
44
|
-
|
|
92
|
+
// Start playing if this is the first item and channel isn't paused
|
|
93
|
+
if (channel.queue.length === 1 && !channel.isPaused) {
|
|
94
|
+
// Use setTimeout to ensure the queue change event is emitted first
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
playAudioQueue(channelNumber).catch((error: Error) => {
|
|
97
|
+
handleAudioError(audio, channelNumber, audioUrl, error);
|
|
98
|
+
});
|
|
99
|
+
}, 0);
|
|
45
100
|
}
|
|
46
101
|
};
|
|
47
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Adds an audio file to the front of the queue in a specific channel
|
|
105
|
+
* This is a convenience function that places the audio right after the currently playing track
|
|
106
|
+
* @param audioUrl - The URL of the audio file to queue
|
|
107
|
+
* @param channelNumber - The channel number to queue the audio to (defaults to 0)
|
|
108
|
+
* @param options - Optional configuration for the audio file
|
|
109
|
+
* @returns Promise that resolves when the audio is queued
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* await queueAudioPriority('./urgent-announcement.wav', 0);
|
|
113
|
+
* await queueAudioPriority('./priority-sound.mp3', 1, { loop: true });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export const queueAudioPriority = async (
|
|
117
|
+
audioUrl: string,
|
|
118
|
+
channelNumber: number = 0,
|
|
119
|
+
options?: AudioQueueOptions
|
|
120
|
+
): Promise<void> => {
|
|
121
|
+
const priorityOptions: AudioQueueOptions = { ...options, addToFront: true };
|
|
122
|
+
return queueAudio(audioUrl, channelNumber, priorityOptions);
|
|
123
|
+
};
|
|
124
|
+
|
|
48
125
|
/**
|
|
49
126
|
* Plays the audio queue for a specific channel
|
|
50
127
|
* @param channelNumber - The channel number to play
|
|
@@ -57,12 +134,20 @@ export const queueAudio = async (audioUrl: string, channelNumber: number = 0): P
|
|
|
57
134
|
export const playAudioQueue = async (channelNumber: number): Promise<void> => {
|
|
58
135
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
59
136
|
|
|
60
|
-
if (channel.queue.length === 0) return;
|
|
137
|
+
if (!channel || channel.queue.length === 0) return;
|
|
61
138
|
|
|
62
139
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
63
140
|
|
|
141
|
+
// Apply channel volume if not already set
|
|
142
|
+
if (currentAudio.volume === 1.0 && channel.volume !== undefined) {
|
|
143
|
+
currentAudio.volume = channel.volume;
|
|
144
|
+
}
|
|
145
|
+
|
|
64
146
|
setupProgressTracking(currentAudio, channelNumber, audioChannels);
|
|
65
147
|
|
|
148
|
+
// Apply volume ducking when audio starts
|
|
149
|
+
await applyVolumeDucking(channelNumber);
|
|
150
|
+
|
|
66
151
|
return new Promise<void>((resolve) => {
|
|
67
152
|
let hasStarted: boolean = false;
|
|
68
153
|
let metadataLoaded: boolean = false;
|
|
@@ -102,19 +187,36 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
|
|
|
102
187
|
src: currentAudio.src
|
|
103
188
|
}, audioChannels);
|
|
104
189
|
|
|
190
|
+
// Restore volume levels when priority channel stops
|
|
191
|
+
await restoreVolumeLevels(channelNumber);
|
|
192
|
+
|
|
105
193
|
// Clean up event listeners
|
|
106
194
|
currentAudio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
107
195
|
currentAudio.removeEventListener('play', handlePlay);
|
|
108
196
|
currentAudio.removeEventListener('ended', handleEnded);
|
|
109
197
|
|
|
110
198
|
cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
|
|
111
|
-
channel.queue.shift();
|
|
112
|
-
|
|
113
|
-
// Emit queue change after completion
|
|
114
|
-
setTimeout(() => emitQueueChange(channelNumber, audioChannels), 10);
|
|
115
199
|
|
|
116
|
-
|
|
117
|
-
|
|
200
|
+
// Handle looping vs non-looping audio
|
|
201
|
+
if (currentAudio.loop) {
|
|
202
|
+
// For looping audio, reset current time and continue playing
|
|
203
|
+
currentAudio.currentTime = 0;
|
|
204
|
+
try {
|
|
205
|
+
await currentAudio.play();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
await handleAudioError(currentAudio, channelNumber, currentAudio.src, error as Error);
|
|
208
|
+
}
|
|
209
|
+
resolve();
|
|
210
|
+
} else {
|
|
211
|
+
// For non-looping audio, remove from queue and play next
|
|
212
|
+
channel.queue.shift();
|
|
213
|
+
|
|
214
|
+
// Emit queue change after completion
|
|
215
|
+
setTimeout(() => emitQueueChange(channelNumber, audioChannels), 10);
|
|
216
|
+
|
|
217
|
+
await playAudioQueue(channelNumber);
|
|
218
|
+
resolve();
|
|
219
|
+
}
|
|
118
220
|
};
|
|
119
221
|
|
|
120
222
|
// Add event listeners
|
|
@@ -127,7 +229,11 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
|
|
|
127
229
|
metadataLoaded = true;
|
|
128
230
|
}
|
|
129
231
|
|
|
130
|
-
|
|
232
|
+
// Enhanced play with error handling
|
|
233
|
+
currentAudio.play().catch(async (error: Error) => {
|
|
234
|
+
await handleAudioError(currentAudio, channelNumber, currentAudio.src, error);
|
|
235
|
+
resolve(); // Resolve to prevent hanging
|
|
236
|
+
});
|
|
131
237
|
});
|
|
132
238
|
};
|
|
133
239
|
|
|
@@ -136,11 +242,11 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
|
|
|
136
242
|
* @param channelNumber - The channel number (defaults to 0)
|
|
137
243
|
* @example
|
|
138
244
|
* ```typescript
|
|
139
|
-
* stopCurrentAudioInChannel(
|
|
140
|
-
* stopCurrentAudioInChannel(); // Stop current audio in
|
|
245
|
+
* await stopCurrentAudioInChannel(); // Stop current audio in default channel (0)
|
|
246
|
+
* await stopCurrentAudioInChannel(1); // Stop current audio in channel 1
|
|
141
247
|
* ```
|
|
142
248
|
*/
|
|
143
|
-
export const stopCurrentAudioInChannel = (channelNumber: number = 0): void => {
|
|
249
|
+
export const stopCurrentAudioInChannel = async (channelNumber: number = 0): Promise<void> => {
|
|
144
250
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
145
251
|
if (channel && channel.queue.length > 0) {
|
|
146
252
|
const currentAudio: HTMLAudioElement = channel.queue[0];
|
|
@@ -152,13 +258,18 @@ export const stopCurrentAudioInChannel = (channelNumber: number = 0): void => {
|
|
|
152
258
|
src: currentAudio.src
|
|
153
259
|
}, audioChannels);
|
|
154
260
|
|
|
261
|
+
// Restore volume levels when stopping
|
|
262
|
+
await restoreVolumeLevels(channelNumber);
|
|
263
|
+
|
|
155
264
|
currentAudio.pause();
|
|
156
265
|
cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
|
|
157
266
|
channel.queue.shift();
|
|
267
|
+
channel.isPaused = false; // Reset pause state
|
|
158
268
|
|
|
159
269
|
emitQueueChange(channelNumber, audioChannels);
|
|
160
270
|
|
|
161
|
-
|
|
271
|
+
// Start next audio without waiting for it to complete
|
|
272
|
+
playAudioQueue(channelNumber).catch(console.error);
|
|
162
273
|
}
|
|
163
274
|
};
|
|
164
275
|
|
|
@@ -167,11 +278,11 @@ export const stopCurrentAudioInChannel = (channelNumber: number = 0): void => {
|
|
|
167
278
|
* @param channelNumber - The channel number (defaults to 0)
|
|
168
279
|
* @example
|
|
169
280
|
* ```typescript
|
|
170
|
-
* stopAllAudioInChannel(
|
|
171
|
-
* stopAllAudioInChannel(); // Clear all audio in
|
|
281
|
+
* await stopAllAudioInChannel(); // Clear all audio in default channel (0)
|
|
282
|
+
* await stopAllAudioInChannel(1); // Clear all audio in channel 1
|
|
172
283
|
* ```
|
|
173
284
|
*/
|
|
174
|
-
export const stopAllAudioInChannel = (channelNumber: number = 0): void => {
|
|
285
|
+
export const stopAllAudioInChannel = async (channelNumber: number = 0): Promise<void> => {
|
|
175
286
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
176
287
|
if (channel) {
|
|
177
288
|
if (channel.queue.length > 0) {
|
|
@@ -184,12 +295,16 @@ export const stopAllAudioInChannel = (channelNumber: number = 0): void => {
|
|
|
184
295
|
src: currentAudio.src
|
|
185
296
|
}, audioChannels);
|
|
186
297
|
|
|
298
|
+
// Restore volume levels when stopping
|
|
299
|
+
await restoreVolumeLevels(channelNumber);
|
|
300
|
+
|
|
187
301
|
currentAudio.pause();
|
|
188
302
|
cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
|
|
189
303
|
}
|
|
190
304
|
// Clean up all progress tracking for this channel
|
|
191
305
|
channel.queue.forEach(audio => cleanupProgressTracking(audio, channelNumber, audioChannels));
|
|
192
306
|
channel.queue = [];
|
|
307
|
+
channel.isPaused = false; // Reset pause state
|
|
193
308
|
|
|
194
309
|
emitQueueChange(channelNumber, audioChannels);
|
|
195
310
|
}
|
|
@@ -199,11 +314,13 @@ export const stopAllAudioInChannel = (channelNumber: number = 0): void => {
|
|
|
199
314
|
* Stops all audio across all channels and clears all queues
|
|
200
315
|
* @example
|
|
201
316
|
* ```typescript
|
|
202
|
-
* stopAllAudio(); // Emergency stop - clears everything
|
|
317
|
+
* await stopAllAudio(); // Emergency stop - clears everything
|
|
203
318
|
* ```
|
|
204
319
|
*/
|
|
205
|
-
export const stopAllAudio = (): void => {
|
|
320
|
+
export const stopAllAudio = async (): Promise<void> => {
|
|
321
|
+
const stopPromises: Promise<void>[] = [];
|
|
206
322
|
audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
|
|
207
|
-
stopAllAudioInChannel(index);
|
|
323
|
+
stopPromises.push(stopAllAudioInChannel(index));
|
|
208
324
|
});
|
|
325
|
+
await Promise.all(stopPromises);
|
|
209
326
|
};
|