audio-channel-queue 1.10.0 → 1.11.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/README.md CHANGED
@@ -362,12 +362,24 @@ onAudioStart(channelNumber, callback);
362
362
  onAudioStart(0, (info) => console.log(`Started: ${info.fileName}`)); // Log audio starts
363
363
  ```
364
364
 
365
+ ```typescript
366
+ // Unsubscribe from audio start events (removes ALL start callbacks for the channel)
367
+ offAudioStart(channelNumber);
368
+ offAudioStart(0); // Stop receiving all start notifications for channel 0
369
+ ```
370
+
365
371
  ```typescript
366
372
  // Subscribe to audio completion events.
367
373
  onAudioComplete(channelNumber, callback);
368
374
  onAudioComplete(0, (info) => logPlayHistory(info)); // Track completed audio
369
375
  ```
370
376
 
377
+ ```typescript
378
+ // Unsubscribe from audio completion events (removes ALL complete callbacks for the channel)
379
+ offAudioComplete(channelNumber);
380
+ offAudioComplete(0); // Stop receiving all completion notifications for channel 0
381
+ ```
382
+
371
383
  ```typescript
372
384
  // Subscribe to audio pause events.
373
385
  onAudioPause(channelNumber, callback);
package/dist/core.d.ts CHANGED
@@ -73,10 +73,10 @@ export declare const queueAudioPriority: (audioUrl: string, channelNumber?: numb
73
73
  /**
74
74
  * Plays the audio queue for a specific channel
75
75
  * @param channelNumber - The channel number to play
76
- * @returns Promise that resolves when the current audio finishes playing
76
+ * @returns Promise that resolves when the audio starts playing (setup complete)
77
77
  * @example
78
78
  * ```typescript
79
- * await playAudioQueue(0); // Play queue for channel 0
79
+ * await playAudioQueue(0); // Start playing queue for channel 0
80
80
  * ```
81
81
  */
82
82
  export declare const playAudioQueue: (channelNumber: number) => Promise<void>;
package/dist/core.js CHANGED
@@ -279,12 +279,8 @@ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...
279
279
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
280
280
  // Start playing if this is the first item and channel isn't paused
281
281
  if (channel.queue.length === 1 && !channel.isPaused) {
282
- // Use setTimeout to ensure the queue change event is emitted first
283
- setTimeout(() => {
284
- (0, exports.playAudioQueue)(channelNumber).catch((error) => {
285
- (0, errors_1.handleAudioError)(audio, channelNumber, validatedUrl, error);
286
- });
287
- }, 0);
282
+ // Await the audio setup to complete before resolving queueAudio
283
+ yield (0, exports.playAudioQueue)(channelNumber);
288
284
  }
289
285
  });
290
286
  exports.queueAudio = queueAudio;
@@ -309,10 +305,10 @@ exports.queueAudioPriority = queueAudioPriority;
309
305
  /**
310
306
  * Plays the audio queue for a specific channel
311
307
  * @param channelNumber - The channel number to play
312
- * @returns Promise that resolves when the current audio finishes playing
308
+ * @returns Promise that resolves when the audio starts playing (setup complete)
313
309
  * @example
314
310
  * ```typescript
315
- * await playAudioQueue(0); // Play queue for channel 0
311
+ * await playAudioQueue(0); // Start playing queue for channel 0
316
312
  * ```
317
313
  */
318
314
  const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
@@ -331,6 +327,7 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
331
327
  let hasStarted = false;
332
328
  let metadataLoaded = false;
333
329
  let playStarted = false;
330
+ let setupComplete = false;
334
331
  // Check if we should fire onAudioStart (both conditions met)
335
332
  const tryFireAudioStart = () => {
336
333
  if (!hasStarted && metadataLoaded && playStarted) {
@@ -341,6 +338,11 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
341
338
  fileName: (0, utils_1.extractFileName)(currentAudio.src),
342
339
  src: currentAudio.src
343
340
  }, info_1.audioChannels);
341
+ // Resolve setup promise when audio start event is fired
342
+ if (!setupComplete) {
343
+ setupComplete = true;
344
+ resolve();
345
+ }
344
346
  }
345
347
  };
346
348
  // Event handler for when metadata loads (duration becomes available)
@@ -371,7 +373,6 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
371
373
  catch (error) {
372
374
  yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
373
375
  }
374
- resolve();
375
376
  }
376
377
  else {
377
378
  // For non-looping audio, remove from queue and play next
@@ -384,7 +385,6 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
384
385
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
385
386
  // Play next audio immediately if there's more in queue
386
387
  yield (0, exports.playAudioQueue)(channelNumber);
387
- resolve();
388
388
  }
389
389
  });
390
390
  // Add event listeners
@@ -398,7 +398,10 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
398
398
  // Enhanced play with error handling
399
399
  currentAudio.play().catch((error) => __awaiter(void 0, void 0, void 0, function* () {
400
400
  yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
401
- resolve(); // Resolve to prevent hanging
401
+ if (!setupComplete) {
402
+ setupComplete = true;
403
+ resolve(); // Resolve gracefully instead of rejecting
404
+ }
402
405
  }));
403
406
  });
404
407
  });
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { clearQueueAfterCurrent, getQueueItemInfo, getQueueLength, removeQueuedI
8
8
  export { getErrorRecovery, getRetryConfig, offAudioError, onAudioError, retryFailedAudio, setErrorRecovery, setRetryConfig } from './errors';
9
9
  export { getAllChannelsPauseState, isChannelPaused, pauseAllChannels, pauseAllWithFade, pauseChannel, pauseWithFade, resumeAllChannels, resumeAllWithFade, resumeChannel, resumeWithFade, togglePauseAllChannels, togglePauseAllWithFade, togglePauseChannel, togglePauseWithFade } from './pause';
10
10
  export { clearVolumeDucking, fadeVolume, getAllChannelsVolume, getChannelVolume, getFadeConfig, setAllChannelsVolume, setChannelVolume, setVolumeDucking, transitionVolume, cancelVolumeTransition, cancelAllVolumeTransitions } from './volume';
11
- export { getAllChannelsInfo, getCurrentAudioInfo, getQueueSnapshot, offAudioPause, offAudioProgress, offAudioResume, offQueueChange, onAudioComplete, onAudioPause, onAudioProgress, onAudioResume, onAudioStart, onQueueChange } from './info';
11
+ export { getAllChannelsInfo, getCurrentAudioInfo, getQueueSnapshot, offAudioComplete, offAudioPause, offAudioProgress, offAudioResume, offAudioStart, offQueueChange, onAudioComplete, onAudioPause, onAudioProgress, onAudioResume, onAudioStart, onQueueChange } from './info';
12
12
  export { audioChannels } from './info';
13
13
  export { cleanWebpackFilename, createQueueSnapshot, extractFileName, getAudioInfoFromElement, sanitizeForDisplay, validateAudioUrl } from './utils';
14
14
  export type { AudioCompleteCallback, AudioCompleteInfo, AudioErrorCallback, AudioErrorInfo, AudioInfo, AudioPauseCallback, AudioQueueOptions, AudioResumeCallback, AudioStartCallback, AudioStartInfo, ChannelFadeState, ErrorRecoveryOptions, ExtendedAudioQueueChannel, FadeConfig, ProgressCallback, QueueChangeCallback, QueueItem, QueueManipulationResult, QueueSnapshot, RetryConfig, VolumeConfig, QueueConfig } from './types';
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.getAllChannelsInfo = exports.cancelAllVolumeTransitions = exports.cancelVolumeTransition = exports.transitionVolume = exports.setVolumeDucking = exports.setChannelVolume = exports.setAllChannelsVolume = exports.getFadeConfig = exports.getChannelVolume = exports.getAllChannelsVolume = exports.fadeVolume = exports.clearVolumeDucking = exports.togglePauseWithFade = exports.togglePauseChannel = exports.togglePauseAllWithFade = exports.togglePauseAllChannels = exports.resumeWithFade = exports.resumeChannel = exports.resumeAllWithFade = exports.resumeAllChannels = exports.pauseWithFade = exports.pauseChannel = exports.pauseAllWithFade = exports.pauseAllChannels = exports.isChannelPaused = exports.getAllChannelsPauseState = exports.setRetryConfig = exports.setErrorRecovery = exports.retryFailedAudio = exports.onAudioError = exports.offAudioError = exports.getRetryConfig = exports.getErrorRecovery = exports.swapQueueItems = exports.reorderQueue = exports.removeQueuedItem = exports.getQueueLength = exports.getQueueItemInfo = exports.clearQueueAfterCurrent = exports.setChannelQueueLimit = exports.getQueueConfig = exports.setQueueConfig = exports.destroyAllChannels = exports.destroyChannel = exports.playAudioQueue = exports.stopAllAudio = exports.stopAllAudioInChannel = exports.stopCurrentAudioInChannel = exports.queueAudioPriority = exports.queueAudio = void 0;
9
- exports.GLOBAL_PROGRESS_KEY = exports.TimerType = exports.MAX_CHANNELS = exports.FadeType = exports.EasingType = exports.validateAudioUrl = exports.sanitizeForDisplay = exports.getAudioInfoFromElement = exports.extractFileName = exports.createQueueSnapshot = exports.cleanWebpackFilename = exports.audioChannels = exports.onQueueChange = exports.onAudioStart = exports.onAudioResume = exports.onAudioProgress = exports.onAudioPause = exports.onAudioComplete = exports.offQueueChange = exports.offAudioResume = exports.offAudioProgress = exports.offAudioPause = exports.getQueueSnapshot = exports.getCurrentAudioInfo = void 0;
9
+ exports.GLOBAL_PROGRESS_KEY = exports.TimerType = exports.MAX_CHANNELS = exports.FadeType = exports.EasingType = exports.validateAudioUrl = exports.sanitizeForDisplay = exports.getAudioInfoFromElement = exports.extractFileName = exports.createQueueSnapshot = exports.cleanWebpackFilename = exports.audioChannels = exports.onQueueChange = exports.onAudioStart = exports.onAudioResume = exports.onAudioProgress = exports.onAudioPause = exports.onAudioComplete = exports.offQueueChange = exports.offAudioStart = exports.offAudioResume = exports.offAudioProgress = exports.offAudioPause = exports.offAudioComplete = exports.getQueueSnapshot = exports.getCurrentAudioInfo = void 0;
10
10
  // Core queue management functions
11
11
  var core_1 = require("./core");
12
12
  Object.defineProperty(exports, "queueAudio", { enumerable: true, get: function () { return core_1.queueAudio; } });
@@ -71,9 +71,11 @@ var info_1 = require("./info");
71
71
  Object.defineProperty(exports, "getAllChannelsInfo", { enumerable: true, get: function () { return info_1.getAllChannelsInfo; } });
72
72
  Object.defineProperty(exports, "getCurrentAudioInfo", { enumerable: true, get: function () { return info_1.getCurrentAudioInfo; } });
73
73
  Object.defineProperty(exports, "getQueueSnapshot", { enumerable: true, get: function () { return info_1.getQueueSnapshot; } });
74
+ Object.defineProperty(exports, "offAudioComplete", { enumerable: true, get: function () { return info_1.offAudioComplete; } });
74
75
  Object.defineProperty(exports, "offAudioPause", { enumerable: true, get: function () { return info_1.offAudioPause; } });
75
76
  Object.defineProperty(exports, "offAudioProgress", { enumerable: true, get: function () { return info_1.offAudioProgress; } });
76
77
  Object.defineProperty(exports, "offAudioResume", { enumerable: true, get: function () { return info_1.offAudioResume; } });
78
+ Object.defineProperty(exports, "offAudioStart", { enumerable: true, get: function () { return info_1.offAudioStart; } });
77
79
  Object.defineProperty(exports, "offQueueChange", { enumerable: true, get: function () { return info_1.offQueueChange; } });
78
80
  Object.defineProperty(exports, "onAudioComplete", { enumerable: true, get: function () { return info_1.onAudioComplete; } });
79
81
  Object.defineProperty(exports, "onAudioPause", { enumerable: true, get: function () { return info_1.onAudioPause; } });
package/dist/info.d.ts CHANGED
@@ -2,6 +2,29 @@
2
2
  * @fileoverview Audio information and progress tracking functions for the audio-channel-queue package
3
3
  */
4
4
  import { AudioInfo, QueueSnapshot, ProgressCallback, QueueChangeCallback, AudioStartCallback, AudioCompleteCallback, AudioPauseCallback, AudioResumeCallback, ExtendedAudioQueueChannel } from './types';
5
+ /**
6
+ * Gets the current list of whitelisted channel properties
7
+ * This is automatically derived from the ExtendedAudioQueueChannel interface
8
+ * @returns Array of whitelisted property names
9
+ * @internal
10
+ */
11
+ export declare const getWhitelistedChannelProperties: () => string[];
12
+ /**
13
+ * Returns the list of non-whitelisted properties found on a specific channel
14
+ * These are properties that will trigger warnings when modified directly
15
+ * @param channelNumber - The channel number to inspect (defaults to 0)
16
+ * @returns Array of property names that are not in the whitelist, or empty array if channel doesn't exist
17
+ * @example
18
+ * ```typescript
19
+ * // Add some custom property to a channel
20
+ * (audioChannels[0] as any).customProperty = 'test';
21
+ *
22
+ * const nonWhitelisted = getNonWhitelistedChannelProperties(0);
23
+ * console.log(nonWhitelisted); // ['customProperty']
24
+ * ```
25
+ * @internal
26
+ */
27
+ export declare const getNonWhitelistedChannelProperties: (channelNumber?: number) => string[];
5
28
  /**
6
29
  * Global array to store audio channels with their queues and callback management
7
30
  * Each channel maintains its own audio queue and event callback sets
@@ -176,3 +199,21 @@ export declare const offAudioPause: (channelNumber: number) => void;
176
199
  * ```
177
200
  */
178
201
  export declare const offAudioResume: (channelNumber: number) => void;
202
+ /**
203
+ * Removes audio start event listeners for a specific channel
204
+ * @param channelNumber - The channel number
205
+ * @example
206
+ * ```typescript
207
+ * offAudioStart(0); // Stop receiving start notifications for channel 0
208
+ * ```
209
+ */
210
+ export declare const offAudioStart: (channelNumber: number) => void;
211
+ /**
212
+ * Removes audio complete event listeners for a specific channel
213
+ * @param channelNumber - The channel number
214
+ * @example
215
+ * ```typescript
216
+ * offAudioComplete(0); // Stop receiving completion notifications for channel 0
217
+ * ```
218
+ */
219
+ export declare const offAudioComplete: (channelNumber: number) => void;
package/dist/info.js CHANGED
@@ -3,11 +3,71 @@
3
3
  * @fileoverview Audio information and progress tracking functions for the audio-channel-queue package
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.offAudioResume = exports.offAudioPause = exports.onAudioResume = exports.onAudioPause = exports.onAudioComplete = exports.onAudioStart = exports.offQueueChange = exports.onQueueChange = exports.onAudioProgress = exports.getQueueSnapshot = exports.getAllChannelsInfo = exports.getCurrentAudioInfo = exports.audioChannels = void 0;
6
+ exports.offAudioComplete = exports.offAudioStart = exports.offAudioResume = exports.offAudioPause = exports.onAudioResume = exports.onAudioPause = exports.onAudioComplete = exports.onAudioStart = exports.offQueueChange = exports.onQueueChange = exports.onAudioProgress = exports.getQueueSnapshot = exports.getAllChannelsInfo = exports.getCurrentAudioInfo = exports.audioChannels = exports.getNonWhitelistedChannelProperties = exports.getWhitelistedChannelProperties = void 0;
7
7
  exports.offAudioProgress = offAudioProgress;
8
8
  const types_1 = require("./types");
9
9
  const utils_1 = require("./utils");
10
10
  const events_1 = require("./events");
11
+ /**
12
+ * Gets the current list of whitelisted channel properties
13
+ * This is automatically derived from the ExtendedAudioQueueChannel interface
14
+ * @returns Array of whitelisted property names
15
+ * @internal
16
+ */
17
+ const getWhitelistedChannelProperties = () => {
18
+ // Create a sample channel object to extract property names
19
+ const sampleChannel = {
20
+ audioCompleteCallbacks: new Set(),
21
+ audioErrorCallbacks: new Set(),
22
+ audioPauseCallbacks: new Set(),
23
+ audioResumeCallbacks: new Set(),
24
+ audioStartCallbacks: new Set(),
25
+ isPaused: false,
26
+ progressCallbacks: new Map(),
27
+ queue: [],
28
+ queueChangeCallbacks: new Set(),
29
+ volume: 1.0
30
+ };
31
+ // Get all property names from the interface (including optional ones)
32
+ const propertyNames = [
33
+ ...Object.getOwnPropertyNames(sampleChannel),
34
+ // Add optional properties that might not be present on the sample
35
+ 'fadeState',
36
+ 'isLocked',
37
+ 'maxQueueSize',
38
+ 'retryConfig',
39
+ 'volumeConfig' // Legacy property that might still be used
40
+ ];
41
+ return [...new Set(propertyNames)]; // Remove duplicates
42
+ };
43
+ exports.getWhitelistedChannelProperties = getWhitelistedChannelProperties;
44
+ /**
45
+ * Returns the list of non-whitelisted properties found on a specific channel
46
+ * These are properties that will trigger warnings when modified directly
47
+ * @param channelNumber - The channel number to inspect (defaults to 0)
48
+ * @returns Array of property names that are not in the whitelist, or empty array if channel doesn't exist
49
+ * @example
50
+ * ```typescript
51
+ * // Add some custom property to a channel
52
+ * (audioChannels[0] as any).customProperty = 'test';
53
+ *
54
+ * const nonWhitelisted = getNonWhitelistedChannelProperties(0);
55
+ * console.log(nonWhitelisted); // ['customProperty']
56
+ * ```
57
+ * @internal
58
+ */
59
+ const getNonWhitelistedChannelProperties = (channelNumber = 0) => {
60
+ const channel = exports.audioChannels[channelNumber];
61
+ if (!channel) {
62
+ return [];
63
+ }
64
+ const whitelistedProperties = (0, exports.getWhitelistedChannelProperties)();
65
+ const allChannelProperties = Object.getOwnPropertyNames(channel);
66
+ // Filter out properties that are in the whitelist
67
+ const nonWhitelistedProperties = allChannelProperties.filter((property) => !whitelistedProperties.includes(property));
68
+ return nonWhitelistedProperties;
69
+ };
70
+ exports.getNonWhitelistedChannelProperties = getNonWhitelistedChannelProperties;
11
71
  /**
12
72
  * Global array to store audio channels with their queues and callback management
13
73
  * Each channel maintains its own audio queue and event callback sets
@@ -35,8 +95,9 @@ exports.audioChannels = new Proxy([], {
35
95
  return new Proxy(value, {
36
96
  set(channelTarget, channelProp, channelValue) {
37
97
  // Allow internal modifications but warn about direct property changes
38
- if (typeof channelProp === 'string' &&
39
- !['queue', 'volume', 'isPaused', 'isLocked', 'volumeConfig'].includes(channelProp)) {
98
+ // Use the automatically-derived whitelist from the interface
99
+ const whitelistedProperties = (0, exports.getWhitelistedChannelProperties)();
100
+ if (typeof channelProp === 'string' && !whitelistedProperties.includes(channelProp)) {
40
101
  // eslint-disable-next-line no-console
41
102
  console.warn(`Warning: Direct modification of channel.${channelProp} detected. ` +
42
103
  'Use API functions for safer channel management.');
@@ -429,3 +490,33 @@ const offAudioResume = (channelNumber) => {
429
490
  channel.audioResumeCallbacks.clear();
430
491
  };
431
492
  exports.offAudioResume = offAudioResume;
493
+ /**
494
+ * Removes audio start event listeners for a specific channel
495
+ * @param channelNumber - The channel number
496
+ * @example
497
+ * ```typescript
498
+ * offAudioStart(0); // Stop receiving start notifications for channel 0
499
+ * ```
500
+ */
501
+ const offAudioStart = (channelNumber) => {
502
+ const channel = exports.audioChannels[channelNumber];
503
+ if (!(channel === null || channel === void 0 ? void 0 : channel.audioStartCallbacks))
504
+ return;
505
+ channel.audioStartCallbacks.clear();
506
+ };
507
+ exports.offAudioStart = offAudioStart;
508
+ /**
509
+ * Removes audio complete event listeners for a specific channel
510
+ * @param channelNumber - The channel number
511
+ * @example
512
+ * ```typescript
513
+ * offAudioComplete(0); // Stop receiving completion notifications for channel 0
514
+ * ```
515
+ */
516
+ const offAudioComplete = (channelNumber) => {
517
+ const channel = exports.audioChannels[channelNumber];
518
+ if (!(channel === null || channel === void 0 ? void 0 : channel.audioCompleteCallbacks))
519
+ return;
520
+ channel.audioCompleteCallbacks.clear();
521
+ };
522
+ exports.offAudioComplete = offAudioComplete;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-channel-queue",
3
- "version": "1.10.0",
3
+ "version": "1.11.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
@@ -320,12 +320,8 @@ export const queueAudio = async (
320
320
 
321
321
  // Start playing if this is the first item and channel isn't paused
322
322
  if (channel.queue.length === 1 && !channel.isPaused) {
323
- // Use setTimeout to ensure the queue change event is emitted first
324
- setTimeout(() => {
325
- playAudioQueue(channelNumber).catch((error: Error) => {
326
- handleAudioError(audio, channelNumber, validatedUrl, error);
327
- });
328
- }, 0);
323
+ // Await the audio setup to complete before resolving queueAudio
324
+ await playAudioQueue(channelNumber);
329
325
  }
330
326
  };
331
327
 
@@ -354,10 +350,10 @@ export const queueAudioPriority = async (
354
350
  /**
355
351
  * Plays the audio queue for a specific channel
356
352
  * @param channelNumber - The channel number to play
357
- * @returns Promise that resolves when the current audio finishes playing
353
+ * @returns Promise that resolves when the audio starts playing (setup complete)
358
354
  * @example
359
355
  * ```typescript
360
- * await playAudioQueue(0); // Play queue for channel 0
356
+ * await playAudioQueue(0); // Start playing queue for channel 0
361
357
  * ```
362
358
  */
363
359
  export const playAudioQueue = async (channelNumber: number): Promise<void> => {
@@ -381,6 +377,7 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
381
377
  let hasStarted: boolean = false;
382
378
  let metadataLoaded: boolean = false;
383
379
  let playStarted: boolean = false;
380
+ let setupComplete: boolean = false;
384
381
 
385
382
  // Check if we should fire onAudioStart (both conditions met)
386
383
  const tryFireAudioStart = (): void => {
@@ -396,6 +393,12 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
396
393
  },
397
394
  audioChannels
398
395
  );
396
+
397
+ // Resolve setup promise when audio start event is fired
398
+ if (!setupComplete) {
399
+ setupComplete = true;
400
+ resolve();
401
+ }
399
402
  }
400
403
  };
401
404
 
@@ -433,7 +436,6 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
433
436
  } catch (error) {
434
437
  await handleAudioError(currentAudio, channelNumber, currentAudio.src, error as Error);
435
438
  }
436
- resolve();
437
439
  } else {
438
440
  // For non-looping audio, remove from queue and play next
439
441
  currentAudio.pause();
@@ -448,7 +450,6 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
448
450
 
449
451
  // Play next audio immediately if there's more in queue
450
452
  await playAudioQueue(channelNumber);
451
- resolve();
452
453
  }
453
454
  };
454
455
 
@@ -465,7 +466,10 @@ export const playAudioQueue = async (channelNumber: number): Promise<void> => {
465
466
  // Enhanced play with error handling
466
467
  currentAudio.play().catch(async (error: Error) => {
467
468
  await handleAudioError(currentAudio, channelNumber, currentAudio.src, error);
468
- resolve(); // Resolve to prevent hanging
469
+ if (!setupComplete) {
470
+ setupComplete = true;
471
+ resolve(); // Resolve gracefully instead of rejecting
472
+ }
469
473
  });
470
474
  });
471
475
  };
package/src/index.ts CHANGED
@@ -78,9 +78,11 @@ export {
78
78
  getAllChannelsInfo,
79
79
  getCurrentAudioInfo,
80
80
  getQueueSnapshot,
81
+ offAudioComplete,
81
82
  offAudioPause,
82
83
  offAudioProgress,
83
84
  offAudioResume,
85
+ offAudioStart,
84
86
  offQueueChange,
85
87
  onAudioComplete,
86
88
  onAudioPause,
package/src/info.ts CHANGED
@@ -18,6 +18,73 @@ import {
18
18
  import { getAudioInfoFromElement, createQueueSnapshot } from './utils';
19
19
  import { setupProgressTracking, cleanupProgressTracking } from './events';
20
20
 
21
+ /**
22
+ * Gets the current list of whitelisted channel properties
23
+ * This is automatically derived from the ExtendedAudioQueueChannel interface
24
+ * @returns Array of whitelisted property names
25
+ * @internal
26
+ */
27
+ export const getWhitelistedChannelProperties = (): string[] => {
28
+ // Create a sample channel object to extract property names
29
+ const sampleChannel: ExtendedAudioQueueChannel = {
30
+ audioCompleteCallbacks: new Set(),
31
+ audioErrorCallbacks: new Set(),
32
+ audioPauseCallbacks: new Set(),
33
+ audioResumeCallbacks: new Set(),
34
+ audioStartCallbacks: new Set(),
35
+ isPaused: false,
36
+ progressCallbacks: new Map(),
37
+ queue: [],
38
+ queueChangeCallbacks: new Set(),
39
+ volume: 1.0
40
+ };
41
+
42
+ // Get all property names from the interface (including optional ones)
43
+ const propertyNames = [
44
+ ...Object.getOwnPropertyNames(sampleChannel),
45
+ // Add optional properties that might not be present on the sample
46
+ 'fadeState',
47
+ 'isLocked',
48
+ 'maxQueueSize',
49
+ 'retryConfig',
50
+ 'volumeConfig' // Legacy property that might still be used
51
+ ];
52
+
53
+ return [...new Set(propertyNames)]; // Remove duplicates
54
+ };
55
+
56
+ /**
57
+ * Returns the list of non-whitelisted properties found on a specific channel
58
+ * These are properties that will trigger warnings when modified directly
59
+ * @param channelNumber - The channel number to inspect (defaults to 0)
60
+ * @returns Array of property names that are not in the whitelist, or empty array if channel doesn't exist
61
+ * @example
62
+ * ```typescript
63
+ * // Add some custom property to a channel
64
+ * (audioChannels[0] as any).customProperty = 'test';
65
+ *
66
+ * const nonWhitelisted = getNonWhitelistedChannelProperties(0);
67
+ * console.log(nonWhitelisted); // ['customProperty']
68
+ * ```
69
+ * @internal
70
+ */
71
+ export const getNonWhitelistedChannelProperties = (channelNumber: number = 0): string[] => {
72
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
73
+ if (!channel) {
74
+ return [];
75
+ }
76
+
77
+ const whitelistedProperties: string[] = getWhitelistedChannelProperties();
78
+ const allChannelProperties: string[] = Object.getOwnPropertyNames(channel);
79
+
80
+ // Filter out properties that are in the whitelist
81
+ const nonWhitelistedProperties: string[] = allChannelProperties.filter(
82
+ (property: string) => !whitelistedProperties.includes(property)
83
+ );
84
+
85
+ return nonWhitelistedProperties;
86
+ };
87
+
21
88
  /**
22
89
  * Global array to store audio channels with their queues and callback management
23
90
  * Each channel maintains its own audio queue and event callback sets
@@ -56,10 +123,10 @@ export const audioChannels: ExtendedAudioQueueChannel[] = new Proxy(
56
123
  channelValue: unknown
57
124
  ): boolean {
58
125
  // Allow internal modifications but warn about direct property changes
59
- if (
60
- typeof channelProp === 'string' &&
61
- !['queue', 'volume', 'isPaused', 'isLocked', 'volumeConfig'].includes(channelProp)
62
- ) {
126
+ // Use the automatically-derived whitelist from the interface
127
+ const whitelistedProperties = getWhitelistedChannelProperties();
128
+
129
+ if (typeof channelProp === 'string' && !whitelistedProperties.includes(channelProp)) {
63
130
  // eslint-disable-next-line no-console
64
131
  console.warn(
65
132
  `Warning: Direct modification of channel.${channelProp} detected. ` +
@@ -484,3 +551,33 @@ export const offAudioResume = (channelNumber: number): void => {
484
551
 
485
552
  channel.audioResumeCallbacks.clear();
486
553
  };
554
+
555
+ /**
556
+ * Removes audio start event listeners for a specific channel
557
+ * @param channelNumber - The channel number
558
+ * @example
559
+ * ```typescript
560
+ * offAudioStart(0); // Stop receiving start notifications for channel 0
561
+ * ```
562
+ */
563
+ export const offAudioStart = (channelNumber: number): void => {
564
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
565
+ if (!channel?.audioStartCallbacks) return;
566
+
567
+ channel.audioStartCallbacks.clear();
568
+ };
569
+
570
+ /**
571
+ * Removes audio complete event listeners for a specific channel
572
+ * @param channelNumber - The channel number
573
+ * @example
574
+ * ```typescript
575
+ * offAudioComplete(0); // Stop receiving completion notifications for channel 0
576
+ * ```
577
+ */
578
+ export const offAudioComplete = (channelNumber: number): void => {
579
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
580
+ if (!channel?.audioCompleteCallbacks) return;
581
+
582
+ channel.audioCompleteCallbacks.clear();
583
+ };