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/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 (audioUrl: string, channelNumber: number = 0): Promise<void> => {
28
- if (!audioChannels[channelNumber]) {
29
- audioChannels[channelNumber] = {
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 (audioChannels[channelNumber].queue.length === 1) {
44
- playAudioQueue(channelNumber);
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
- await playAudioQueue(channelNumber);
117
- resolve();
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
- currentAudio.play();
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(0); // Stop current audio in channel 0
140
- * stopCurrentAudioInChannel(); // Stop current audio in default channel
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
- playAudioQueue(channelNumber);
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(0); // Clear all audio in channel 0
171
- * stopAllAudioInChannel(); // Clear all audio in default channel
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
  };