audio-channel-queue 1.11.0 → 1.12.1-beta.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/errors.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import {
6
6
  AudioErrorInfo,
7
7
  AudioErrorCallback,
8
+ AudioErrorType,
8
9
  RetryConfig,
9
10
  ErrorRecoveryOptions,
10
11
  ExtendedAudioQueueChannel,
@@ -17,6 +18,7 @@ let globalRetryConfig: RetryConfig = {
17
18
  baseDelay: 1000,
18
19
  enabled: true,
19
20
  exponentialBackoff: true,
21
+ fallbackUrls: [],
20
22
  maxRetries: 3,
21
23
  skipOnFailure: false,
22
24
  timeoutMs: 10000
@@ -32,8 +34,6 @@ let globalErrorRecovery: ErrorRecoveryOptions = {
32
34
 
33
35
  const retryAttempts: WeakMap<HTMLAudioElement, number> = new WeakMap();
34
36
 
35
- const loadTimeouts: WeakMap<HTMLAudioElement, number> = new WeakMap();
36
-
37
37
  /**
38
38
  * Subscribes to audio error events for a specific channel
39
39
  * @param channelNumber - The channel number to listen to (defaults to 0)
@@ -143,10 +143,10 @@ export const getRetryConfig = (): RetryConfig => {
143
143
  * ```typescript
144
144
  * setErrorRecovery({
145
145
  * autoRetry: true,
146
- * showUserFeedback: true,
146
+ * fallbackToNextTrack: true,
147
147
  * logErrorsToAnalytics: true,
148
148
  * preserveQueueOnError: true,
149
- * fallbackToNextTrack: true
149
+ * showUserFeedback: true
150
150
  * });
151
151
  * ```
152
152
  */
@@ -247,14 +247,11 @@ export const emitAudioError = (
247
247
  * @returns The categorized error type
248
248
  * @internal
249
249
  */
250
- export const categorizeError = (
251
- error: Error,
252
- audio: HTMLAudioElement
253
- ): AudioErrorInfo['errorType'] => {
250
+ export const categorizeError = (error: Error, audio: HTMLAudioElement): AudioErrorType => {
254
251
  const errorMessage = error.message.toLowerCase();
255
252
 
256
253
  if (errorMessage.includes('network') || errorMessage.includes('fetch')) {
257
- return 'network';
254
+ return AudioErrorType.Network;
258
255
  }
259
256
 
260
257
  // Check for unsupported format first (more specific than decode)
@@ -263,35 +260,35 @@ export const categorizeError = (
263
260
  errorMessage.includes('unsupported') ||
264
261
  errorMessage.includes('format not supported')
265
262
  ) {
266
- return 'unsupported';
263
+ return AudioErrorType.Unsupported;
267
264
  }
268
265
 
269
266
  if (errorMessage.includes('decode') || errorMessage.includes('format')) {
270
- return 'decode';
267
+ return AudioErrorType.Decode;
271
268
  }
272
269
 
273
270
  if (errorMessage.includes('permission') || errorMessage.includes('blocked')) {
274
- return 'permission';
271
+ return AudioErrorType.Permission;
275
272
  }
276
273
 
277
274
  if (errorMessage.includes('abort')) {
278
- return 'abort';
275
+ return AudioErrorType.Abort;
279
276
  }
280
277
 
281
278
  if (errorMessage.includes('timeout')) {
282
- return 'timeout';
279
+ return AudioErrorType.Timeout;
283
280
  }
284
281
 
285
282
  // Check audio element network state for more context
286
283
  if (audio.networkState === HTMLMediaElement.NETWORK_NO_SOURCE) {
287
- return 'network';
284
+ return AudioErrorType.Network;
288
285
  }
289
286
 
290
287
  if (audio.networkState === HTMLMediaElement.NETWORK_LOADING) {
291
- return 'timeout';
288
+ return AudioErrorType.Timeout;
292
289
  }
293
290
 
294
- return 'unknown';
291
+ return AudioErrorType.Unknown;
295
292
  };
296
293
 
297
294
  /**
@@ -311,42 +308,8 @@ export const setupAudioErrorHandling = (
311
308
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
312
309
  if (!channel) return;
313
310
 
314
- // Set up loading timeout with test environment compatibility
315
- let timeoutId: number;
316
- if (typeof setTimeout !== 'undefined') {
317
- timeoutId = setTimeout(() => {
318
- if (audio.networkState === HTMLMediaElement.NETWORK_LOADING) {
319
- const timeoutError = new Error(
320
- `Audio loading timeout after ${globalRetryConfig.timeoutMs}ms`
321
- );
322
- handleAudioError(audio, channelNumber, originalUrl, timeoutError);
323
- }
324
- }, globalRetryConfig.timeoutMs) as unknown as number;
325
-
326
- loadTimeouts.set(audio, timeoutId);
327
- }
328
-
329
- // Clear timeout when metadata loads successfully
330
- const handleLoadSuccess = (): void => {
331
- if (typeof setTimeout !== 'undefined') {
332
- const timeoutId = loadTimeouts.get(audio);
333
- if (timeoutId) {
334
- clearTimeout(timeoutId);
335
- loadTimeouts.delete(audio);
336
- }
337
- }
338
- };
339
-
340
311
  // Handle various error events
341
312
  const handleError = (_event: Event): void => {
342
- if (typeof setTimeout !== 'undefined') {
343
- const timeoutId = loadTimeouts.get(audio);
344
- if (timeoutId) {
345
- clearTimeout(timeoutId);
346
- loadTimeouts.delete(audio);
347
- }
348
- }
349
-
350
313
  const error = new Error(`Audio loading failed: ${audio.error?.message || 'Unknown error'}`);
351
314
  handleAudioError(audio, channelNumber, originalUrl, error);
352
315
  };
@@ -365,8 +328,6 @@ export const setupAudioErrorHandling = (
365
328
  audio.addEventListener('error', handleError);
366
329
  audio.addEventListener('abort', handleAbort);
367
330
  audio.addEventListener('stalled', handleStall);
368
- audio.addEventListener('loadedmetadata', handleLoadSuccess);
369
- audio.addEventListener('canplay', handleLoadSuccess);
370
331
 
371
332
  // Custom play error handling
372
333
  if (onError) {
@@ -424,7 +385,7 @@ export const handleAudioError = async (
424
385
  currentAttempts < retryConfig.maxRetries &&
425
386
  globalErrorRecovery.autoRetry
426
387
  ) {
427
- const delay = retryConfig.exponentialBackoff
388
+ const delay: number = retryConfig.exponentialBackoff
428
389
  ? retryConfig.baseDelay * Math.pow(2, currentAttempts)
429
390
  : retryConfig.baseDelay;
430
391
 
@@ -482,21 +443,11 @@ export const createProtectedAudioElement = async (
482
443
  const audio = new Audio();
483
444
 
484
445
  return new Promise((resolve, reject) => {
485
- const cleanup = (): void => {
486
- const timeoutId = loadTimeouts.get(audio);
487
- if (timeoutId) {
488
- clearTimeout(timeoutId);
489
- loadTimeouts.delete(audio);
490
- }
491
- };
492
-
493
446
  const handleSuccess = (): void => {
494
- cleanup();
495
447
  resolve(audio);
496
448
  };
497
449
 
498
450
  const handleError = (error: Error): void => {
499
- cleanup();
500
451
  reject(error);
501
452
  };
502
453
 
package/src/index.ts CHANGED
@@ -60,19 +60,34 @@ export {
60
60
 
61
61
  // Volume control and ducking functions
62
62
  export {
63
+ cancelAllVolumeTransitions,
64
+ cancelVolumeTransition,
63
65
  clearVolumeDucking,
64
- fadeVolume,
65
66
  getAllChannelsVolume,
66
67
  getChannelVolume,
67
68
  getFadeConfig,
68
69
  setAllChannelsVolume,
69
70
  setChannelVolume,
70
71
  setVolumeDucking,
71
- transitionVolume,
72
- cancelVolumeTransition,
73
- cancelAllVolumeTransitions
72
+ transitionVolume
74
73
  } from './volume';
75
74
 
75
+ // Web Audio API support functions
76
+ export {
77
+ cleanupWebAudioNodes,
78
+ createWebAudioNodes,
79
+ getAudioContext,
80
+ getWebAudioConfig,
81
+ getWebAudioSupport,
82
+ getWebAudioVolume,
83
+ isIOSDevice,
84
+ isWebAudioSupported,
85
+ resumeAudioContext,
86
+ setWebAudioConfig,
87
+ setWebAudioVolume,
88
+ shouldUseWebAudio
89
+ } from './web-audio';
90
+
76
91
  // Audio information and progress tracking functions
77
92
  export {
78
93
  getAllChannelsInfo,
@@ -123,13 +138,23 @@ export type {
123
138
  FadeConfig,
124
139
  ProgressCallback,
125
140
  QueueChangeCallback,
141
+ QueueConfig,
126
142
  QueueItem,
127
143
  QueueManipulationResult,
128
144
  QueueSnapshot,
129
145
  RetryConfig,
130
146
  VolumeConfig,
131
- QueueConfig
147
+ WebAudioConfig,
148
+ WebAudioNodeSet,
149
+ WebAudioSupport
132
150
  } from './types';
133
151
 
134
152
  // Enums and constants
135
- export { EasingType, FadeType, MAX_CHANNELS, TimerType, GLOBAL_PROGRESS_KEY } from './types';
153
+ export {
154
+ AudioErrorType,
155
+ EasingType,
156
+ FadeType,
157
+ MAX_CHANNELS,
158
+ TimerType,
159
+ GLOBAL_PROGRESS_KEY
160
+ } from './types';
package/src/info.ts CHANGED
@@ -351,13 +351,14 @@ export const onQueueChange = (channelNumber: number, callback: QueueChangeCallba
351
351
 
352
352
  /**
353
353
  * Removes queue change listeners for a specific channel
354
- * @param channelNumber - The channel number
354
+ * @param channelNumber - The channel number (defaults to 0)
355
355
  * @example
356
356
  * ```typescript
357
- * offQueueChange(0); // Stop receiving queue change notifications for channel 0
357
+ * offQueueChange(); // Stop receiving queue change notifications for default channel (0)
358
+ * offQueueChange(1); // Stop receiving queue change notifications for channel 1
358
359
  * ```
359
360
  */
360
- export const offQueueChange = (channelNumber: number): void => {
361
+ export const offQueueChange = (channelNumber: number = 0): void => {
361
362
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
362
363
  if (!channel?.queueChangeCallbacks) return;
363
364
 
@@ -524,13 +525,14 @@ export const onAudioResume = (channelNumber: number, callback: AudioResumeCallba
524
525
 
525
526
  /**
526
527
  * Removes pause event listeners for a specific channel
527
- * @param channelNumber - The channel number
528
+ * @param channelNumber - The channel number (defaults to 0)
528
529
  * @example
529
530
  * ```typescript
530
- * offAudioPause(0); // Stop receiving pause notifications for channel 0
531
+ * offAudioPause(); // Stop receiving pause notifications for default channel (0)
532
+ * offAudioPause(1); // Stop receiving pause notifications for channel 1
531
533
  * ```
532
534
  */
533
- export const offAudioPause = (channelNumber: number): void => {
535
+ export const offAudioPause = (channelNumber: number = 0): void => {
534
536
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
535
537
  if (!channel?.audioPauseCallbacks) return;
536
538
 
@@ -539,13 +541,14 @@ export const offAudioPause = (channelNumber: number): void => {
539
541
 
540
542
  /**
541
543
  * Removes resume event listeners for a specific channel
542
- * @param channelNumber - The channel number
544
+ * @param channelNumber - The channel number (defaults to 0)
543
545
  * @example
544
546
  * ```typescript
545
- * offAudioResume(0); // Stop receiving resume notifications for channel 0
547
+ * offAudioResume(); // Stop receiving resume notifications for default channel (0)
548
+ * offAudioResume(1); // Stop receiving resume notifications for channel 1
546
549
  * ```
547
550
  */
548
- export const offAudioResume = (channelNumber: number): void => {
551
+ export const offAudioResume = (channelNumber: number = 0): void => {
549
552
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
550
553
  if (!channel?.audioResumeCallbacks) return;
551
554
 
@@ -554,13 +557,14 @@ export const offAudioResume = (channelNumber: number): void => {
554
557
 
555
558
  /**
556
559
  * Removes audio start event listeners for a specific channel
557
- * @param channelNumber - The channel number
560
+ * @param channelNumber - The channel number (defaults to 0)
558
561
  * @example
559
562
  * ```typescript
560
- * offAudioStart(0); // Stop receiving start notifications for channel 0
563
+ * offAudioStart(); // Stop receiving start notifications for default channel (0)
564
+ * offAudioStart(1); // Stop receiving start notifications for channel 1
561
565
  * ```
562
566
  */
563
- export const offAudioStart = (channelNumber: number): void => {
567
+ export const offAudioStart = (channelNumber: number = 0): void => {
564
568
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
565
569
  if (!channel?.audioStartCallbacks) return;
566
570
 
@@ -569,13 +573,14 @@ export const offAudioStart = (channelNumber: number): void => {
569
573
 
570
574
  /**
571
575
  * Removes audio complete event listeners for a specific channel
572
- * @param channelNumber - The channel number
576
+ * @param channelNumber - The channel number (defaults to 0)
573
577
  * @example
574
578
  * ```typescript
575
- * offAudioComplete(0); // Stop receiving completion notifications for channel 0
579
+ * offAudioComplete(); // Stop receiving completion notifications for default channel (0)
580
+ * offAudioComplete(1); // Stop receiving completion notifications for channel 1
576
581
  * ```
577
582
  */
578
- export const offAudioComplete = (channelNumber: number): void => {
583
+ export const offAudioComplete = (channelNumber: number = 0): void => {
579
584
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
580
585
  if (!channel?.audioCompleteCallbacks) return;
581
586
 
package/src/types.ts CHANGED
@@ -29,15 +29,15 @@ export interface AudioQueueChannel {
29
29
  * Volume ducking configuration for channels
30
30
  */
31
31
  export interface VolumeConfig {
32
+ /** Duration in milliseconds for volume duck transition (defaults to 250ms) */
33
+ duckTransitionDuration?: number;
34
+ /** Volume level for all other channels when priority channel is active (0-1) */
35
+ duckingVolume: number;
32
36
  /** The channel number that should have priority */
33
37
  priorityChannel: number;
34
38
  /** Volume level for the priority channel (0-1) */
35
39
  priorityVolume: number;
36
- /** Volume level for all other channels when priority channel is active (0-1) */
37
- duckingVolume: number;
38
- /** Duration in milliseconds for volume duck transition (defaults to 250ms) */
39
- duckTransitionDuration?: number;
40
- /** Duration in milliseconds for volume restore transition (defaults to 500ms) */
40
+ /** Duration in milliseconds for volume restore transition (defaults to 250ms) */
41
41
  restoreTransitionDuration?: number;
42
42
  /** Easing function for volume transitions (defaults to 'ease-out') */
43
43
  transitionEasing?: EasingType;
@@ -53,8 +53,6 @@ export interface AudioQueueOptions {
53
53
  loop?: boolean;
54
54
  /** Maximum number of items allowed in the queue (defaults to unlimited) */
55
55
  maxQueueSize?: number;
56
- /** @deprecated Use addToFront instead. Legacy support for priority queuing */
57
- priority?: boolean;
58
56
  /** Volume level for this specific audio (0-1) */
59
57
  volume?: number;
60
58
  }
@@ -212,41 +210,74 @@ export type AudioPauseCallback = (channelNumber: number, audioInfo: AudioInfo) =
212
210
  export type AudioResumeCallback = (channelNumber: number, audioInfo: AudioInfo) => void;
213
211
 
214
212
  /**
215
- * Information about an audio error that occurred
213
+ * Types of audio errors that can occur during playback
214
+ */
215
+ export enum AudioErrorType {
216
+ Abort = 'abort',
217
+ Decode = 'decode',
218
+ Network = 'network',
219
+ Permission = 'permission',
220
+ Timeout = 'timeout',
221
+ Unknown = 'unknown',
222
+ Unsupported = 'unsupported'
223
+ }
224
+
225
+ /**
226
+ * Information about an audio error that occurred during playback or loading
216
227
  */
217
228
  export interface AudioErrorInfo {
229
+ /** Channel number where the error occurred */
218
230
  channelNumber: number;
219
- src: string;
220
- fileName: string;
231
+ /** The actual error object that was thrown */
221
232
  error: Error;
222
- errorType: 'network' | 'decode' | 'unsupported' | 'permission' | 'abort' | 'timeout' | 'unknown';
223
- timestamp: number;
224
- retryAttempt?: number;
233
+ /** Categorized type of error for handling different scenarios */
234
+ errorType: AudioErrorType;
235
+ /** Extracted filename from the source URL */
236
+ fileName: string;
237
+ /** Number of audio files remaining in the queue after this error */
225
238
  remainingInQueue: number;
239
+ /** Current retry attempt number (if retrying is enabled) */
240
+ retryAttempt?: number;
241
+ /** Audio file source URL that failed */
242
+ src: string;
243
+ /** Unix timestamp when the error occurred */
244
+ timestamp: number;
226
245
  }
227
246
 
228
247
  /**
229
248
  * Configuration for automatic retry behavior when audio fails to load or play
230
249
  */
231
250
  export interface RetryConfig {
232
- enabled: boolean;
233
- maxRetries: number;
251
+ /** Initial delay in milliseconds before first retry attempt */
234
252
  baseDelay: number;
253
+ /** Whether automatic retries are enabled for this channel */
254
+ enabled: boolean;
255
+ /** Whether to use exponential backoff (doubling delay each retry) */
235
256
  exponentialBackoff: boolean;
236
- timeoutMs: number;
237
- fallbackUrls?: string[];
257
+ /** Alternative URLs to try if the primary source fails */
258
+ fallbackUrls: string[];
259
+ /** Maximum number of retry attempts before giving up */
260
+ maxRetries: number;
261
+ /** Whether to skip to next track in queue if all retries fail */
238
262
  skipOnFailure: boolean;
263
+ /** Timeout in milliseconds for each individual retry attempt */
264
+ timeoutMs: number;
239
265
  }
240
266
 
241
267
  /**
242
- * Configuration options for error recovery mechanisms
268
+ * Configuration options for error recovery mechanisms across the audio system
243
269
  */
244
270
  export interface ErrorRecoveryOptions {
271
+ /** Whether to automatically retry failed audio loads/plays */
245
272
  autoRetry: boolean;
246
- showUserFeedback: boolean;
273
+ /** Whether to automatically skip to next track when current fails */
274
+ fallbackToNextTrack: boolean;
275
+ /** Whether to send error data to analytics systems */
247
276
  logErrorsToAnalytics: boolean;
277
+ /** Whether to maintain queue integrity when errors occur */
248
278
  preserveQueueOnError: boolean;
249
- fallbackToNextTrack: boolean;
279
+ /** Whether to display user-visible error feedback */
280
+ showUserFeedback: boolean;
250
281
  }
251
282
 
252
283
  /**
@@ -255,29 +286,81 @@ export interface ErrorRecoveryOptions {
255
286
  export type AudioErrorCallback = (errorInfo: AudioErrorInfo) => void;
256
287
 
257
288
  /**
258
- * Extended audio channel with queue management and callback support
289
+ * Web Audio API configuration options
290
+ */
291
+ export interface WebAudioConfig {
292
+ /** Whether to automatically use Web Audio API on iOS devices */
293
+ autoDetectIOS: boolean;
294
+ /** Whether Web Audio API support is enabled */
295
+ enabled: boolean;
296
+ /** Whether to force Web Audio API usage on all devices */
297
+ forceWebAudio: boolean;
298
+ }
299
+
300
+ /**
301
+ * Web Audio API support information
302
+ */
303
+ export interface WebAudioSupport {
304
+ /** Whether Web Audio API is available in the current environment */
305
+ available: boolean;
306
+ /** Whether the current device is iOS */
307
+ isIOS: boolean;
308
+ /** Whether Web Audio API is currently being used */
309
+ usingWebAudio: boolean;
310
+ /** Reason for current Web Audio API usage state */
311
+ reason: string;
312
+ }
313
+
314
+ /**
315
+ * Web Audio API node set for audio element control
316
+ */
317
+ export interface WebAudioNodeSet {
318
+ /** Gain node for volume control */
319
+ gainNode: GainNode;
320
+ /** Media element source node */
321
+ sourceNode: MediaElementAudioSourceNode;
322
+ }
323
+
324
+ /**
325
+ * Extended audio channel with comprehensive queue management, callback support, and state tracking
259
326
  */
260
327
  export interface ExtendedAudioQueueChannel {
328
+ /** Set of callbacks triggered when audio completes playback */
261
329
  audioCompleteCallbacks: Set<AudioCompleteCallback>;
330
+ /** Set of callbacks triggered when audio errors occur */
262
331
  audioErrorCallbacks: Set<AudioErrorCallback>;
332
+ /** Set of callbacks triggered when audio is paused */
263
333
  audioPauseCallbacks: Set<AudioPauseCallback>;
334
+ /** Set of callbacks triggered when audio is resumed */
264
335
  audioResumeCallbacks: Set<AudioResumeCallback>;
336
+ /** Set of callbacks triggered when audio starts playing */
265
337
  audioStartCallbacks: Set<AudioStartCallback>;
338
+ /** Current fade state if pause/resume with fade is active */
266
339
  fadeState?: ChannelFadeState;
340
+ /** Whether the channel is currently paused */
267
341
  isPaused: boolean;
268
342
  /** Active operation lock to prevent race conditions */
269
343
  isLocked?: boolean;
270
344
  /** Maximum allowed queue size for this channel */
271
345
  maxQueueSize?: number;
346
+ /** Map of progress callbacks keyed by audio element or global symbol */
272
347
  progressCallbacks: Map<HTMLAudioElement | typeof GLOBAL_PROGRESS_KEY, Set<ProgressCallback>>;
348
+ /** Array of HTMLAudioElement objects in the queue */
273
349
  queue: HTMLAudioElement[];
350
+ /** Set of callbacks triggered when the queue changes */
274
351
  queueChangeCallbacks: Set<QueueChangeCallback>;
352
+ /** Retry configuration for failed audio loads/plays */
275
353
  retryConfig?: RetryConfig;
354
+ /** Current volume level for the channel (0-1) */
276
355
  volume: number;
356
+ /** Web Audio API context for this channel */
357
+ webAudioContext?: AudioContext;
358
+ /** Map of Web Audio API nodes for each audio element */
359
+ webAudioNodes?: Map<HTMLAudioElement, WebAudioNodeSet>;
277
360
  }
278
361
 
279
362
  /**
280
- * Easing function types for volume transitions
363
+ * Easing function types for smooth volume transitions and animations
281
364
  */
282
365
  export enum EasingType {
283
366
  Linear = 'linear',
@@ -287,7 +370,7 @@ export enum EasingType {
287
370
  }
288
371
 
289
372
  /**
290
- * Fade type for pause/resume operations with integrated volume transitions
373
+ * Predefined fade types for pause/resume operations with different transition characteristics
291
374
  */
292
375
  export enum FadeType {
293
376
  Linear = 'linear',
@@ -296,7 +379,7 @@ export enum FadeType {
296
379
  }
297
380
 
298
381
  /**
299
- * Timer types for volume transitions to ensure proper cleanup
382
+ * Timer implementation types used for volume transitions to ensure proper cleanup
300
383
  */
301
384
  export enum TimerType {
302
385
  RequestAnimationFrame = 'raf',