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/dist/core.js CHANGED
@@ -12,39 +12,106 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.stopAllAudio = exports.stopAllAudioInChannel = exports.stopCurrentAudioInChannel = exports.playAudioQueue = exports.queueAudio = void 0;
15
+ exports.stopAllAudio = exports.stopAllAudioInChannel = exports.stopCurrentAudioInChannel = exports.playAudioQueue = exports.queueAudioPriority = exports.queueAudio = void 0;
16
16
  const info_1 = require("./info");
17
17
  const utils_1 = require("./utils");
18
18
  const events_1 = require("./events");
19
+ const volume_1 = require("./volume");
20
+ const errors_1 = require("./errors");
19
21
  /**
20
22
  * Queues an audio file to a specific channel and starts playing if it's the first in queue
21
23
  * @param audioUrl - The URL of the audio file to queue
22
24
  * @param channelNumber - The channel number to queue the audio to (defaults to 0)
25
+ * @param options - Optional configuration for the audio file
23
26
  * @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
24
27
  * @example
25
28
  * ```typescript
26
29
  * await queueAudio('https://example.com/song.mp3', 0);
27
30
  * await queueAudio('./sounds/notification.wav'); // Uses default channel 0
31
+ * await queueAudio('./music/loop.mp3', 1, { loop: true }); // Loop the audio
32
+ * await queueAudio('./urgent.wav', 0, { addToFront: true }); // Add to front of queue
28
33
  * ```
29
34
  */
30
- const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0) {
31
- if (!info_1.audioChannels[channelNumber]) {
32
- info_1.audioChannels[channelNumber] = {
35
+ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0, options) {
36
+ // Ensure the channel exists
37
+ while (info_1.audioChannels.length <= channelNumber) {
38
+ info_1.audioChannels.push({
33
39
  audioCompleteCallbacks: new Set(),
40
+ audioErrorCallbacks: new Set(),
41
+ audioPauseCallbacks: new Set(),
42
+ audioResumeCallbacks: new Set(),
34
43
  audioStartCallbacks: new Set(),
44
+ isPaused: false,
35
45
  progressCallbacks: new Map(),
36
46
  queue: [],
37
- queueChangeCallbacks: new Set()
38
- };
47
+ queueChangeCallbacks: new Set(),
48
+ volume: 1.0
49
+ });
39
50
  }
51
+ const channel = info_1.audioChannels[channelNumber];
40
52
  const audio = new Audio(audioUrl);
41
- info_1.audioChannels[channelNumber].queue.push(audio);
53
+ // Set up comprehensive error handling
54
+ (0, errors_1.setupAudioErrorHandling)(audio, channelNumber, audioUrl, (error) => __awaiter(void 0, void 0, void 0, function* () {
55
+ yield (0, errors_1.handleAudioError)(audio, channelNumber, audioUrl, error);
56
+ }));
57
+ // Apply options if provided
58
+ if (options) {
59
+ if (typeof options.loop === 'boolean') {
60
+ audio.loop = options.loop;
61
+ }
62
+ if (typeof options.volume === 'number' && !isNaN(options.volume)) {
63
+ const clampedVolume = Math.max(0, Math.min(1, options.volume));
64
+ audio.volume = clampedVolume;
65
+ // Set channel volume to match the audio volume
66
+ channel.volume = clampedVolume;
67
+ }
68
+ }
69
+ // Handle priority option (same as addToFront for backward compatibility)
70
+ const shouldAddToFront = (options === null || options === void 0 ? void 0 : options.addToFront) || (options === null || options === void 0 ? void 0 : options.priority);
71
+ // Add to queue based on priority/addToFront option
72
+ if (shouldAddToFront && channel.queue.length > 0) {
73
+ // Insert after currently playing track (at index 1)
74
+ channel.queue.splice(1, 0, audio);
75
+ }
76
+ else if (shouldAddToFront) {
77
+ // If queue is empty, add to front
78
+ channel.queue.unshift(audio);
79
+ }
80
+ else {
81
+ // Add to back of queue
82
+ channel.queue.push(audio);
83
+ }
84
+ // Emit queue change event
42
85
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
43
- if (info_1.audioChannels[channelNumber].queue.length === 1) {
44
- (0, exports.playAudioQueue)(channelNumber);
86
+ // Start playing if this is the first item and channel isn't paused
87
+ if (channel.queue.length === 1 && !channel.isPaused) {
88
+ // Use setTimeout to ensure the queue change event is emitted first
89
+ setTimeout(() => {
90
+ (0, exports.playAudioQueue)(channelNumber).catch((error) => {
91
+ (0, errors_1.handleAudioError)(audio, channelNumber, audioUrl, error);
92
+ });
93
+ }, 0);
45
94
  }
46
95
  });
47
96
  exports.queueAudio = queueAudio;
97
+ /**
98
+ * Adds an audio file to the front of the queue in a specific channel
99
+ * This is a convenience function that places the audio right after the currently playing track
100
+ * @param audioUrl - The URL of the audio file to queue
101
+ * @param channelNumber - The channel number to queue the audio to (defaults to 0)
102
+ * @param options - Optional configuration for the audio file
103
+ * @returns Promise that resolves when the audio is queued
104
+ * @example
105
+ * ```typescript
106
+ * await queueAudioPriority('./urgent-announcement.wav', 0);
107
+ * await queueAudioPriority('./priority-sound.mp3', 1, { loop: true });
108
+ * ```
109
+ */
110
+ const queueAudioPriority = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0, options) {
111
+ const priorityOptions = Object.assign(Object.assign({}, options), { addToFront: true });
112
+ return (0, exports.queueAudio)(audioUrl, channelNumber, priorityOptions);
113
+ });
114
+ exports.queueAudioPriority = queueAudioPriority;
48
115
  /**
49
116
  * Plays the audio queue for a specific channel
50
117
  * @param channelNumber - The channel number to play
@@ -56,10 +123,16 @@ exports.queueAudio = queueAudio;
56
123
  */
57
124
  const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
58
125
  const channel = info_1.audioChannels[channelNumber];
59
- if (channel.queue.length === 0)
126
+ if (!channel || channel.queue.length === 0)
60
127
  return;
61
128
  const currentAudio = channel.queue[0];
129
+ // Apply channel volume if not already set
130
+ if (currentAudio.volume === 1.0 && channel.volume !== undefined) {
131
+ currentAudio.volume = channel.volume;
132
+ }
62
133
  (0, events_1.setupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
134
+ // Apply volume ducking when audio starts
135
+ yield (0, volume_1.applyVolumeDucking)(channelNumber);
63
136
  return new Promise((resolve) => {
64
137
  let hasStarted = false;
65
138
  let metadataLoaded = false;
@@ -94,16 +167,33 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
94
167
  remainingInQueue: channel.queue.length - 1,
95
168
  src: currentAudio.src
96
169
  }, info_1.audioChannels);
170
+ // Restore volume levels when priority channel stops
171
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
97
172
  // Clean up event listeners
98
173
  currentAudio.removeEventListener('loadedmetadata', handleLoadedMetadata);
99
174
  currentAudio.removeEventListener('play', handlePlay);
100
175
  currentAudio.removeEventListener('ended', handleEnded);
101
176
  (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
102
- channel.queue.shift();
103
- // Emit queue change after completion
104
- setTimeout(() => (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels), 10);
105
- yield (0, exports.playAudioQueue)(channelNumber);
106
- resolve();
177
+ // Handle looping vs non-looping audio
178
+ if (currentAudio.loop) {
179
+ // For looping audio, reset current time and continue playing
180
+ currentAudio.currentTime = 0;
181
+ try {
182
+ yield currentAudio.play();
183
+ }
184
+ catch (error) {
185
+ yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
186
+ }
187
+ resolve();
188
+ }
189
+ else {
190
+ // For non-looping audio, remove from queue and play next
191
+ channel.queue.shift();
192
+ // Emit queue change after completion
193
+ setTimeout(() => (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels), 10);
194
+ yield (0, exports.playAudioQueue)(channelNumber);
195
+ resolve();
196
+ }
107
197
  });
108
198
  // Add event listeners
109
199
  currentAudio.addEventListener('loadedmetadata', handleLoadedMetadata);
@@ -113,7 +203,11 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
113
203
  if (currentAudio.readyState >= 1) { // HAVE_METADATA or higher
114
204
  metadataLoaded = true;
115
205
  }
116
- currentAudio.play();
206
+ // Enhanced play with error handling
207
+ currentAudio.play().catch((error) => __awaiter(void 0, void 0, void 0, function* () {
208
+ yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
209
+ resolve(); // Resolve to prevent hanging
210
+ }));
117
211
  });
118
212
  });
119
213
  exports.playAudioQueue = playAudioQueue;
@@ -122,11 +216,11 @@ exports.playAudioQueue = playAudioQueue;
122
216
  * @param channelNumber - The channel number (defaults to 0)
123
217
  * @example
124
218
  * ```typescript
125
- * stopCurrentAudioInChannel(0); // Stop current audio in channel 0
126
- * stopCurrentAudioInChannel(); // Stop current audio in default channel
219
+ * await stopCurrentAudioInChannel(); // Stop current audio in default channel (0)
220
+ * await stopCurrentAudioInChannel(1); // Stop current audio in channel 1
127
221
  * ```
128
222
  */
129
- const stopCurrentAudioInChannel = (channelNumber = 0) => {
223
+ const stopCurrentAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
130
224
  const channel = info_1.audioChannels[channelNumber];
131
225
  if (channel && channel.queue.length > 0) {
132
226
  const currentAudio = channel.queue[0];
@@ -136,24 +230,28 @@ const stopCurrentAudioInChannel = (channelNumber = 0) => {
136
230
  remainingInQueue: channel.queue.length - 1,
137
231
  src: currentAudio.src
138
232
  }, info_1.audioChannels);
233
+ // Restore volume levels when stopping
234
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
139
235
  currentAudio.pause();
140
236
  (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
141
237
  channel.queue.shift();
238
+ channel.isPaused = false; // Reset pause state
142
239
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
143
- (0, exports.playAudioQueue)(channelNumber);
240
+ // Start next audio without waiting for it to complete
241
+ (0, exports.playAudioQueue)(channelNumber).catch(console.error);
144
242
  }
145
- };
243
+ });
146
244
  exports.stopCurrentAudioInChannel = stopCurrentAudioInChannel;
147
245
  /**
148
246
  * Stops all audio in a specific channel and clears the entire queue
149
247
  * @param channelNumber - The channel number (defaults to 0)
150
248
  * @example
151
249
  * ```typescript
152
- * stopAllAudioInChannel(0); // Clear all audio in channel 0
153
- * stopAllAudioInChannel(); // Clear all audio in default channel
250
+ * await stopAllAudioInChannel(); // Clear all audio in default channel (0)
251
+ * await stopAllAudioInChannel(1); // Clear all audio in channel 1
154
252
  * ```
155
253
  */
156
- const stopAllAudioInChannel = (channelNumber = 0) => {
254
+ const stopAllAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
157
255
  const channel = info_1.audioChannels[channelNumber];
158
256
  if (channel) {
159
257
  if (channel.queue.length > 0) {
@@ -164,26 +262,31 @@ const stopAllAudioInChannel = (channelNumber = 0) => {
164
262
  remainingInQueue: 0, // Will be 0 since we're clearing the queue
165
263
  src: currentAudio.src
166
264
  }, info_1.audioChannels);
265
+ // Restore volume levels when stopping
266
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
167
267
  currentAudio.pause();
168
268
  (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
169
269
  }
170
270
  // Clean up all progress tracking for this channel
171
271
  channel.queue.forEach(audio => (0, events_1.cleanupProgressTracking)(audio, channelNumber, info_1.audioChannels));
172
272
  channel.queue = [];
273
+ channel.isPaused = false; // Reset pause state
173
274
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
174
275
  }
175
- };
276
+ });
176
277
  exports.stopAllAudioInChannel = stopAllAudioInChannel;
177
278
  /**
178
279
  * Stops all audio across all channels and clears all queues
179
280
  * @example
180
281
  * ```typescript
181
- * stopAllAudio(); // Emergency stop - clears everything
282
+ * await stopAllAudio(); // Emergency stop - clears everything
182
283
  * ```
183
284
  */
184
- const stopAllAudio = () => {
285
+ const stopAllAudio = () => __awaiter(void 0, void 0, void 0, function* () {
286
+ const stopPromises = [];
185
287
  info_1.audioChannels.forEach((_channel, index) => {
186
- (0, exports.stopAllAudioInChannel)(index);
288
+ stopPromises.push((0, exports.stopAllAudioInChannel)(index));
187
289
  });
188
- };
290
+ yield Promise.all(stopPromises);
291
+ });
189
292
  exports.stopAllAudio = stopAllAudio;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @fileoverview Error handling, retry logic, and recovery mechanisms for the audio-channel-queue package
3
+ */
4
+ import { AudioErrorInfo, AudioErrorCallback, RetryConfig, ErrorRecoveryOptions, ExtendedAudioQueueChannel } from './types';
5
+ /**
6
+ * Subscribes to audio error events for a specific channel
7
+ * @param channelNumber - The channel number to listen to (defaults to 0)
8
+ * @param callback - Function to call when an audio error occurs
9
+ * @example
10
+ * ```typescript
11
+ * onAudioError(0, (errorInfo) => {
12
+ * console.log(`Audio error: ${errorInfo.error.message}`);
13
+ * console.log(`Error type: ${errorInfo.errorType}`);
14
+ * });
15
+ * ```
16
+ */
17
+ export declare const onAudioError: (channelNumber: number | undefined, callback: AudioErrorCallback) => void;
18
+ /**
19
+ * Unsubscribes from audio error events for a specific channel
20
+ * @param channelNumber - The channel number to stop listening to (defaults to 0)
21
+ * @param callback - The specific callback to remove (optional - if not provided, removes all)
22
+ * @example
23
+ * ```typescript
24
+ * offAudioError(0); // Remove all error callbacks for channel 0
25
+ * offAudioError(0, specificCallback); // Remove specific callback
26
+ * ```
27
+ */
28
+ export declare const offAudioError: (channelNumber?: number, callback?: AudioErrorCallback) => void;
29
+ /**
30
+ * Sets the global retry configuration for audio loading failures
31
+ * @param config - Retry configuration options
32
+ * @example
33
+ * ```typescript
34
+ * setRetryConfig({
35
+ * enabled: true,
36
+ * maxRetries: 5,
37
+ * baseDelay: 1000,
38
+ * exponentialBackoff: true,
39
+ * timeoutMs: 15000,
40
+ * fallbackUrls: ['https://cdn.backup.com/audio/'],
41
+ * skipOnFailure: true
42
+ * });
43
+ * ```
44
+ */
45
+ export declare const setRetryConfig: (config: Partial<RetryConfig>) => void;
46
+ /**
47
+ * Gets the current global retry configuration
48
+ * @returns Current retry configuration
49
+ * @example
50
+ * ```typescript
51
+ * const config = getRetryConfig();
52
+ * console.log(`Max retries: ${config.maxRetries}`);
53
+ * ```
54
+ */
55
+ export declare const getRetryConfig: () => RetryConfig;
56
+ /**
57
+ * Sets the global error recovery configuration
58
+ * @param options - Error recovery options
59
+ * @example
60
+ * ```typescript
61
+ * setErrorRecovery({
62
+ * autoRetry: true,
63
+ * showUserFeedback: true,
64
+ * logErrorsToAnalytics: true,
65
+ * preserveQueueOnError: true,
66
+ * fallbackToNextTrack: true
67
+ * });
68
+ * ```
69
+ */
70
+ export declare const setErrorRecovery: (options: Partial<ErrorRecoveryOptions>) => void;
71
+ /**
72
+ * Gets the current global error recovery configuration
73
+ * @returns Current error recovery configuration
74
+ * @example
75
+ * ```typescript
76
+ * const recovery = getErrorRecovery();
77
+ * console.log(`Auto retry enabled: ${recovery.autoRetry}`);
78
+ * ```
79
+ */
80
+ export declare const getErrorRecovery: () => ErrorRecoveryOptions;
81
+ /**
82
+ * Manually retries loading failed audio for a specific channel
83
+ * @param channelNumber - The channel number to retry (defaults to 0)
84
+ * @returns Promise that resolves to true if retry was successful, false otherwise
85
+ * @example
86
+ * ```typescript
87
+ * const success = await retryFailedAudio(0);
88
+ * if (success) {
89
+ * console.log('Audio retry successful');
90
+ * } else {
91
+ * console.log('Audio retry failed');
92
+ * }
93
+ * ```
94
+ */
95
+ export declare const retryFailedAudio: (channelNumber?: number) => Promise<boolean>;
96
+ /**
97
+ * Emits an audio error event to all registered listeners for a specific channel
98
+ * @param channelNumber - The channel number where the error occurred
99
+ * @param errorInfo - Information about the error
100
+ * @param audioChannels - Array of audio channels
101
+ * @internal
102
+ */
103
+ export declare const emitAudioError: (channelNumber: number, errorInfo: AudioErrorInfo, audioChannels: ExtendedAudioQueueChannel[]) => void;
104
+ /**
105
+ * Determines the error type based on the error object and context
106
+ * @param error - The error that occurred
107
+ * @param audio - The audio element that failed
108
+ * @returns The categorized error type
109
+ * @internal
110
+ */
111
+ export declare const categorizeError: (error: Error, audio: HTMLAudioElement) => AudioErrorInfo["errorType"];
112
+ /**
113
+ * Sets up comprehensive error handling for an audio element
114
+ * @param audio - The audio element to set up error handling for
115
+ * @param channelNumber - The channel number this audio belongs to
116
+ * @param originalUrl - The original URL that was requested
117
+ * @param onError - Callback for when an error occurs
118
+ * @internal
119
+ */
120
+ export declare const setupAudioErrorHandling: (audio: HTMLAudioElement, channelNumber: number, originalUrl: string, onError?: (error: Error) => Promise<void>) => void;
121
+ /**
122
+ * Handles audio errors with retry logic and recovery mechanisms
123
+ * @param audio - The audio element that failed
124
+ * @param channelNumber - The channel number
125
+ * @param originalUrl - The original URL that was requested
126
+ * @param error - The error that occurred
127
+ * @internal
128
+ */
129
+ export declare const handleAudioError: (audio: HTMLAudioElement, channelNumber: number, originalUrl: string, error: Error) => Promise<void>;
130
+ /**
131
+ * Creates a timeout-protected audio element with comprehensive error handling
132
+ * @param url - The audio URL to load
133
+ * @param channelNumber - The channel number this audio belongs to
134
+ * @returns Promise that resolves to the configured audio element
135
+ * @internal
136
+ */
137
+ export declare const createProtectedAudioElement: (url: string, channelNumber: number) => Promise<HTMLAudioElement>;