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/volume.js CHANGED
@@ -12,18 +12,41 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.restoreVolumeLevels = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = void 0;
15
+ exports.restoreVolumeLevels = exports.fadeVolume = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = exports.getFadeConfig = void 0;
16
+ const types_1 = require("./types");
16
17
  const info_1 = require("./info");
17
18
  // Store active volume transitions to handle interruptions
18
19
  const activeTransitions = new Map();
20
+ /**
21
+ * Predefined fade configurations for different transition types
22
+ */
23
+ const FADE_CONFIGS = {
24
+ [types_1.FadeType.Dramatic]: { duration: 800, pauseCurve: types_1.EasingType.EaseIn, resumeCurve: types_1.EasingType.EaseOut },
25
+ [types_1.FadeType.Gentle]: { duration: 800, pauseCurve: types_1.EasingType.EaseOut, resumeCurve: types_1.EasingType.EaseIn },
26
+ [types_1.FadeType.Linear]: { duration: 800, pauseCurve: types_1.EasingType.Linear, resumeCurve: types_1.EasingType.Linear }
27
+ };
28
+ /**
29
+ * Gets the fade configuration for a specific fade type
30
+ * @param fadeType - The fade type to get configuration for
31
+ * @returns Fade configuration object
32
+ * @example
33
+ * ```typescript
34
+ * const config = getFadeConfig('gentle');
35
+ * console.log(`Gentle fade duration: ${config.duration}ms`);
36
+ * ```
37
+ */
38
+ const getFadeConfig = (fadeType) => {
39
+ return Object.assign({}, FADE_CONFIGS[fadeType]);
40
+ };
41
+ exports.getFadeConfig = getFadeConfig;
19
42
  /**
20
43
  * Easing functions for smooth volume transitions
21
44
  */
22
45
  const easingFunctions = {
23
- linear: (t) => t,
24
- 'ease-in': (t) => t * t,
25
- 'ease-out': (t) => t * (2 - t),
26
- 'ease-in-out': (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
46
+ [types_1.EasingType.Linear]: (t) => t,
47
+ [types_1.EasingType.EaseIn]: (t) => t * t,
48
+ [types_1.EasingType.EaseOut]: (t) => t * (2 - t),
49
+ [types_1.EasingType.EaseInOut]: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
27
50
  };
28
51
  /**
29
52
  * Smoothly transitions volume for a specific channel over time
@@ -37,7 +60,7 @@ const easingFunctions = {
37
60
  * await transitionVolume(0, 0.2, 500, 'ease-out'); // Duck to 20% over 500ms
38
61
  * ```
39
62
  */
40
- const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(void 0, [channelNumber_1, targetVolume_1, ...args_1], void 0, function* (channelNumber, targetVolume, duration = 250, easing = 'ease-out') {
63
+ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(void 0, [channelNumber_1, targetVolume_1, ...args_1], void 0, function* (channelNumber, targetVolume, duration = 250, easing = types_1.EasingType.EaseOut) {
41
64
  const channel = info_1.audioChannels[channelNumber];
42
65
  if (!channel || channel.queue.length === 0)
43
66
  return;
@@ -55,7 +78,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
55
78
  return Promise.resolve();
56
79
  }
57
80
  // Handle zero duration - instant change
58
- if (duration <= 0) {
81
+ if (duration === 0) {
59
82
  channel.volume = targetVolume;
60
83
  if (channel.queue.length > 0) {
61
84
  channel.queue[0].volume = targetVolume;
@@ -115,6 +138,7 @@ const setChannelVolume = (channelNumber, volume, transitionDuration, easing) =>
115
138
  if (!info_1.audioChannels[channelNumber]) {
116
139
  info_1.audioChannels[channelNumber] = {
117
140
  audioCompleteCallbacks: new Set(),
141
+ audioErrorCallbacks: new Set(),
118
142
  audioPauseCallbacks: new Set(),
119
143
  audioResumeCallbacks: new Set(),
120
144
  audioStartCallbacks: new Set(),
@@ -207,6 +231,7 @@ const setVolumeDucking = (config) => {
207
231
  while (info_1.audioChannels.length <= config.priorityChannel) {
208
232
  info_1.audioChannels.push({
209
233
  audioCompleteCallbacks: new Set(),
234
+ audioErrorCallbacks: new Set(),
210
235
  audioPauseCallbacks: new Set(),
211
236
  audioResumeCallbacks: new Set(),
212
237
  audioStartCallbacks: new Set(),
@@ -222,6 +247,7 @@ const setVolumeDucking = (config) => {
222
247
  if (!info_1.audioChannels[index]) {
223
248
  info_1.audioChannels[index] = {
224
249
  audioCompleteCallbacks: new Set(),
250
+ audioErrorCallbacks: new Set(),
225
251
  audioPauseCallbacks: new Set(),
226
252
  audioResumeCallbacks: new Set(),
227
253
  audioStartCallbacks: new Set(),
@@ -263,7 +289,7 @@ const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, vo
263
289
  const config = channel.volumeConfig;
264
290
  if (activeChannelNumber === config.priorityChannel) {
265
291
  const duration = config.duckTransitionDuration || 250;
266
- const easing = config.transitionEasing || 'ease-out';
292
+ const easing = config.transitionEasing || types_1.EasingType.EaseOut;
267
293
  // Priority channel is active, duck other channels
268
294
  if (channelNumber === config.priorityChannel) {
269
295
  transitionPromises.push((0, exports.transitionVolume)(channelNumber, config.priorityVolume, duration, easing));
@@ -278,6 +304,23 @@ const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, vo
278
304
  yield Promise.all(transitionPromises);
279
305
  });
280
306
  exports.applyVolumeDucking = applyVolumeDucking;
307
+ /**
308
+ * Fades the volume for a specific channel over time (alias for transitionVolume with improved naming)
309
+ * @param channelNumber - The channel number to fade
310
+ * @param targetVolume - Target volume level (0-1)
311
+ * @param duration - Fade duration in milliseconds (defaults to 250)
312
+ * @param easing - Easing function type (defaults to 'ease-out')
313
+ * @returns Promise that resolves when fade completes
314
+ * @example
315
+ * ```typescript
316
+ * await fadeVolume(0, 0, 800, 'ease-in'); // Fade out over 800ms
317
+ * await fadeVolume(0, 1, 600, 'ease-out'); // Fade in over 600ms
318
+ * ```
319
+ */
320
+ const fadeVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(void 0, [channelNumber_1, targetVolume_1, ...args_1], void 0, function* (channelNumber, targetVolume, duration = 250, easing = types_1.EasingType.EaseOut) {
321
+ return (0, exports.transitionVolume)(channelNumber, targetVolume, duration, easing);
322
+ });
323
+ exports.fadeVolume = fadeVolume;
281
324
  /**
282
325
  * Restores normal volume levels when priority channel stops with smooth transitions
283
326
  * @param stoppedChannelNumber - The channel that just stopped playing
@@ -290,7 +333,7 @@ const restoreVolumeLevels = (stoppedChannelNumber) => __awaiter(void 0, void 0,
290
333
  const config = channel.volumeConfig;
291
334
  if (stoppedChannelNumber === config.priorityChannel) {
292
335
  const duration = config.restoreTransitionDuration || 500;
293
- const easing = config.transitionEasing || 'ease-out';
336
+ const easing = config.transitionEasing || types_1.EasingType.EaseOut;
294
337
  // Priority channel stopped, restore normal volumes
295
338
  transitionPromises.push((0, exports.transitionVolume)(channelNumber, channel.volume || 1.0, duration, easing));
296
339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-channel-queue",
3
- "version": "1.6.0",
3
+ "version": "1.8.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