audio-channel-queue 1.6.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/types.d.ts CHANGED
@@ -159,25 +159,58 @@ export type AudioPauseCallback = (channelNumber: number, audioInfo: AudioInfo) =
159
159
  */
160
160
  export type AudioResumeCallback = (channelNumber: number, audioInfo: AudioInfo) => void;
161
161
  /**
162
- * Extended audio queue channel with event callback management and additional features
163
- */
164
- export type ExtendedAudioQueueChannel = AudioQueueChannel & {
165
- /** Set of callbacks for audio completion events */
166
- audioCompleteCallbacks?: Set<AudioCompleteCallback>;
167
- /** Set of callbacks for audio pause events */
168
- audioPauseCallbacks?: Set<AudioPauseCallback>;
169
- /** Set of callbacks for audio resume events */
170
- audioResumeCallbacks?: Set<AudioResumeCallback>;
171
- /** Set of callbacks for audio start events */
172
- audioStartCallbacks?: Set<AudioStartCallback>;
173
- /** Whether the current audio in this channel is paused */
162
+ * Information about an audio error that occurred
163
+ */
164
+ export interface AudioErrorInfo {
165
+ channelNumber: number;
166
+ src: string;
167
+ fileName: string;
168
+ error: Error;
169
+ errorType: 'network' | 'decode' | 'unsupported' | 'permission' | 'abort' | 'timeout' | 'unknown';
170
+ timestamp: number;
171
+ retryAttempt?: number;
172
+ remainingInQueue: number;
173
+ }
174
+ /**
175
+ * Configuration for automatic retry behavior when audio fails to load or play
176
+ */
177
+ export interface RetryConfig {
178
+ enabled: boolean;
179
+ maxRetries: number;
180
+ baseDelay: number;
181
+ exponentialBackoff: boolean;
182
+ timeoutMs: number;
183
+ fallbackUrls?: string[];
184
+ skipOnFailure: boolean;
185
+ }
186
+ /**
187
+ * Configuration options for error recovery mechanisms
188
+ */
189
+ export interface ErrorRecoveryOptions {
190
+ autoRetry: boolean;
191
+ showUserFeedback: boolean;
192
+ logErrorsToAnalytics: boolean;
193
+ preserveQueueOnError: boolean;
194
+ fallbackToNextTrack: boolean;
195
+ }
196
+ /**
197
+ * Callback function type for audio error events
198
+ */
199
+ export type AudioErrorCallback = (errorInfo: AudioErrorInfo) => void;
200
+ /**
201
+ * Extended audio queue channel with error handling capabilities
202
+ */
203
+ export interface ExtendedAudioQueueChannel {
204
+ audioCompleteCallbacks: Set<AudioCompleteCallback>;
205
+ audioErrorCallbacks: Set<AudioErrorCallback>;
206
+ audioPauseCallbacks: Set<AudioPauseCallback>;
207
+ audioResumeCallbacks: Set<AudioResumeCallback>;
208
+ audioStartCallbacks: Set<AudioStartCallback>;
174
209
  isPaused?: boolean;
175
- /** Map of audio elements to their progress callback sets */
176
- progressCallbacks?: Map<HTMLAudioElement, Set<ProgressCallback>>;
177
- /** Set of callbacks for queue change events */
178
- queueChangeCallbacks?: Set<QueueChangeCallback>;
179
- /** Current volume level for this channel (0-1) */
210
+ progressCallbacks: Map<HTMLAudioElement | null, Set<ProgressCallback>>;
211
+ queue: HTMLAudioElement[];
212
+ queueChangeCallbacks: Set<QueueChangeCallback>;
213
+ retryConfig?: RetryConfig;
180
214
  volume?: number;
181
- /** Volume ducking configuration for this channel */
182
215
  volumeConfig?: VolumeConfig;
183
- };
216
+ }
package/dist/volume.js CHANGED
@@ -55,7 +55,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
55
55
  return Promise.resolve();
56
56
  }
57
57
  // Handle zero duration - instant change
58
- if (duration <= 0) {
58
+ if (duration === 0) {
59
59
  channel.volume = targetVolume;
60
60
  if (channel.queue.length > 0) {
61
61
  channel.queue[0].volume = targetVolume;
@@ -115,6 +115,7 @@ const setChannelVolume = (channelNumber, volume, transitionDuration, easing) =>
115
115
  if (!info_1.audioChannels[channelNumber]) {
116
116
  info_1.audioChannels[channelNumber] = {
117
117
  audioCompleteCallbacks: new Set(),
118
+ audioErrorCallbacks: new Set(),
118
119
  audioPauseCallbacks: new Set(),
119
120
  audioResumeCallbacks: new Set(),
120
121
  audioStartCallbacks: new Set(),
@@ -207,6 +208,7 @@ const setVolumeDucking = (config) => {
207
208
  while (info_1.audioChannels.length <= config.priorityChannel) {
208
209
  info_1.audioChannels.push({
209
210
  audioCompleteCallbacks: new Set(),
211
+ audioErrorCallbacks: new Set(),
210
212
  audioPauseCallbacks: new Set(),
211
213
  audioResumeCallbacks: new Set(),
212
214
  audioStartCallbacks: new Set(),
@@ -222,6 +224,7 @@ const setVolumeDucking = (config) => {
222
224
  if (!info_1.audioChannels[index]) {
223
225
  info_1.audioChannels[index] = {
224
226
  audioCompleteCallbacks: new Set(),
227
+ audioErrorCallbacks: new Set(),
225
228
  audioPauseCallbacks: new Set(),
226
229
  audioResumeCallbacks: new Set(),
227
230
  audioStartCallbacks: new Set(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-channel-queue",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Allows you to queue audio files to different playback channels.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/core.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  cleanupProgressTracking
14
14
  } from './events';
15
15
  import { applyVolumeDucking, restoreVolumeLevels } from './volume';
16
+ import { setupAudioErrorHandling, handleAudioError } from './errors';
16
17
 
17
18
  /**
18
19
  * Queues an audio file to a specific channel and starts playing if it's the first in queue
@@ -33,9 +34,11 @@ export const queueAudio = async (
33
34
  channelNumber: number = 0,
34
35
  options?: AudioQueueOptions
35
36
  ): Promise<void> => {
36
- if (!audioChannels[channelNumber]) {
37
- audioChannels[channelNumber] = {
37
+ // Ensure the channel exists
38
+ while (audioChannels.length <= channelNumber) {
39
+ audioChannels.push({
38
40
  audioCompleteCallbacks: new Set(),
41
+ audioErrorCallbacks: new Set(),
39
42
  audioPauseCallbacks: new Set(),
40
43
  audioResumeCallbacks: new Set(),
41
44
  audioStartCallbacks: new Set(),
@@ -44,46 +47,56 @@ export const queueAudio = async (
44
47
  queue: [],
45
48
  queueChangeCallbacks: new Set(),
46
49
  volume: 1.0
47
- };
50
+ });
48
51
  }
49
52
 
53
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
50
54
  const audio: HTMLAudioElement = new Audio(audioUrl);
51
-
52
- // Apply audio configuration from options
53
- if (options?.loop) {
54
- audio.loop = true;
55
- }
56
-
57
- if (options?.volume !== undefined) {
58
- const clampedVolume: number = Math.max(0, Math.min(1, options.volume));
59
- // Handle NaN case - default to channel volume or 1.0
60
- const safeVolume: number = isNaN(clampedVolume) ? (audioChannels[channelNumber].volume || 1.0) : clampedVolume;
61
- audio.volume = safeVolume;
62
- // Also update the channel volume
63
- audioChannels[channelNumber].volume = safeVolume;
64
- } else {
65
- // Use channel volume if no specific volume is set
66
- const channelVolume: number = audioChannels[channelNumber].volume || 1.0;
67
- audio.volume = channelVolume;
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
+ }
68
72
  }
69
73
 
70
- // Add to front or back of queue based on options
71
- if ((options?.addToFront || options?.priority) && audioChannels[channelNumber].queue.length > 0) {
72
- // Insert after the currently playing audio (index 1)
73
- audioChannels[channelNumber].queue.splice(1, 0, audio);
74
- } else if ((options?.addToFront || options?.priority) && audioChannels[channelNumber].queue.length === 0) {
75
- // If queue is empty, just add normally
76
- audioChannels[channelNumber].queue.push(audio);
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);
77
84
  } else {
78
- // Default behavior - add to back of queue
79
- audioChannels[channelNumber].queue.push(audio);
85
+ // Add to back of queue
86
+ channel.queue.push(audio);
80
87
  }
81
88
 
89
+ // Emit queue change event
82
90
  emitQueueChange(channelNumber, audioChannels);
83
91
 
84
- if (audioChannels[channelNumber].queue.length === 1) {
85
- // Don't await - let playback happen asynchronously
86
- playAudioQueue(channelNumber).catch(console.error);
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);
87
100
  }
88
101
  };
89
102
 
@@ -188,8 +201,11 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
188
201
  if (currentAudio.loop) {
189
202
  // For looping audio, reset current time and continue playing
190
203
  currentAudio.currentTime = 0;
191
- await currentAudio.play();
192
- // Don't remove from queue, but resolve the promise so tests don't hang
204
+ try {
205
+ await currentAudio.play();
206
+ } catch (error) {
207
+ await handleAudioError(currentAudio, channelNumber, currentAudio.src, error as Error);
208
+ }
193
209
  resolve();
194
210
  } else {
195
211
  // For non-looping audio, remove from queue and play next
@@ -213,7 +229,11 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
213
229
  metadataLoaded = true;
214
230
  }
215
231
 
216
- 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
+ });
217
237
  });
218
238
  };
219
239