audio-channel-queue 1.6.0 → 1.8.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
@@ -17,6 +17,7 @@ const info_1 = require("./info");
17
17
  const utils_1 = require("./utils");
18
18
  const events_1 = require("./events");
19
19
  const volume_1 = require("./volume");
20
+ const errors_1 = require("./errors");
20
21
  /**
21
22
  * Queues an audio file to a specific channel and starts playing if it's the first in queue
22
23
  * @param audioUrl - The URL of the audio file to queue
@@ -32,9 +33,11 @@ const volume_1 = require("./volume");
32
33
  * ```
33
34
  */
34
35
  const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0, options) {
35
- if (!info_1.audioChannels[channelNumber]) {
36
- info_1.audioChannels[channelNumber] = {
36
+ // Ensure the channel exists
37
+ while (info_1.audioChannels.length <= channelNumber) {
38
+ info_1.audioChannels.push({
37
39
  audioCompleteCallbacks: new Set(),
40
+ audioErrorCallbacks: new Set(),
38
41
  audioPauseCallbacks: new Set(),
39
42
  audioResumeCallbacks: new Set(),
40
43
  audioStartCallbacks: new Set(),
@@ -43,43 +46,51 @@ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...
43
46
  queue: [],
44
47
  queueChangeCallbacks: new Set(),
45
48
  volume: 1.0
46
- };
49
+ });
47
50
  }
51
+ const channel = info_1.audioChannels[channelNumber];
48
52
  const audio = new Audio(audioUrl);
49
- // Apply audio configuration from options
50
- if (options === null || options === void 0 ? void 0 : options.loop) {
51
- audio.loop = true;
52
- }
53
- if ((options === null || options === void 0 ? void 0 : options.volume) !== undefined) {
54
- const clampedVolume = Math.max(0, Math.min(1, options.volume));
55
- // Handle NaN case - default to channel volume or 1.0
56
- const safeVolume = isNaN(clampedVolume) ? (info_1.audioChannels[channelNumber].volume || 1.0) : clampedVolume;
57
- audio.volume = safeVolume;
58
- // Also update the channel volume
59
- info_1.audioChannels[channelNumber].volume = safeVolume;
60
- }
61
- else {
62
- // Use channel volume if no specific volume is set
63
- const channelVolume = info_1.audioChannels[channelNumber].volume || 1.0;
64
- audio.volume = channelVolume;
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
+ }
65
68
  }
66
- // Add to front or back of queue based on options
67
- if (((options === null || options === void 0 ? void 0 : options.addToFront) || (options === null || options === void 0 ? void 0 : options.priority)) && info_1.audioChannels[channelNumber].queue.length > 0) {
68
- // Insert after the currently playing audio (index 1)
69
- info_1.audioChannels[channelNumber].queue.splice(1, 0, audio);
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);
70
75
  }
71
- else if (((options === null || options === void 0 ? void 0 : options.addToFront) || (options === null || options === void 0 ? void 0 : options.priority)) && info_1.audioChannels[channelNumber].queue.length === 0) {
72
- // If queue is empty, just add normally
73
- info_1.audioChannels[channelNumber].queue.push(audio);
76
+ else if (shouldAddToFront) {
77
+ // If queue is empty, add to front
78
+ channel.queue.unshift(audio);
74
79
  }
75
80
  else {
76
- // Default behavior - add to back of queue
77
- info_1.audioChannels[channelNumber].queue.push(audio);
81
+ // Add to back of queue
82
+ channel.queue.push(audio);
78
83
  }
84
+ // Emit queue change event
79
85
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
80
- if (info_1.audioChannels[channelNumber].queue.length === 1) {
81
- // Don't await - let playback happen asynchronously
82
- (0, exports.playAudioQueue)(channelNumber).catch(console.error);
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);
83
94
  }
84
95
  });
85
96
  exports.queueAudio = queueAudio;
@@ -167,8 +178,12 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
167
178
  if (currentAudio.loop) {
168
179
  // For looping audio, reset current time and continue playing
169
180
  currentAudio.currentTime = 0;
170
- yield currentAudio.play();
171
- // Don't remove from queue, but resolve the promise so tests don't hang
181
+ try {
182
+ yield currentAudio.play();
183
+ }
184
+ catch (error) {
185
+ yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
186
+ }
172
187
  resolve();
173
188
  }
174
189
  else {
@@ -188,7 +203,11 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
188
203
  if (currentAudio.readyState >= 1) { // HAVE_METADATA or higher
189
204
  metadataLoaded = true;
190
205
  }
191
- 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
+ }));
192
211
  });
193
212
  });
194
213
  exports.playAudioQueue = playAudioQueue;
@@ -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>;