audio-channel-queue 1.9.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/src/index.ts CHANGED
@@ -11,9 +11,24 @@ export {
11
11
  stopCurrentAudioInChannel,
12
12
  stopAllAudioInChannel,
13
13
  stopAllAudio,
14
- playAudioQueue
14
+ playAudioQueue,
15
+ destroyChannel,
16
+ destroyAllChannels,
17
+ setQueueConfig,
18
+ getQueueConfig,
19
+ setChannelQueueLimit
15
20
  } from './core';
16
21
 
22
+ // Queue manipulation functions
23
+ export {
24
+ clearQueueAfterCurrent,
25
+ getQueueItemInfo,
26
+ getQueueLength,
27
+ removeQueuedItem,
28
+ reorderQueue,
29
+ swapQueueItems
30
+ } from './queue-manipulation';
31
+
17
32
  // Error handling and recovery functions
18
33
  export {
19
34
  getErrorRecovery,
@@ -53,7 +68,9 @@ export {
53
68
  setAllChannelsVolume,
54
69
  setChannelVolume,
55
70
  setVolumeDucking,
56
- transitionVolume
71
+ transitionVolume,
72
+ cancelVolumeTransition,
73
+ cancelAllVolumeTransitions
57
74
  } from './volume';
58
75
 
59
76
  // Audio information and progress tracking functions
@@ -61,9 +78,11 @@ export {
61
78
  getAllChannelsInfo,
62
79
  getCurrentAudioInfo,
63
80
  getQueueSnapshot,
81
+ offAudioComplete,
64
82
  offAudioPause,
65
83
  offAudioProgress,
66
84
  offAudioResume,
85
+ offAudioStart,
67
86
  offQueueChange,
68
87
  onAudioComplete,
69
88
  onAudioPause,
@@ -81,7 +100,9 @@ export {
81
100
  cleanWebpackFilename,
82
101
  createQueueSnapshot,
83
102
  extractFileName,
84
- getAudioInfoFromElement
103
+ getAudioInfoFromElement,
104
+ sanitizeForDisplay,
105
+ validateAudioUrl
85
106
  } from './utils';
86
107
 
87
108
  // TypeScript type definitions and interfaces
@@ -103,10 +124,12 @@ export type {
103
124
  ProgressCallback,
104
125
  QueueChangeCallback,
105
126
  QueueItem,
127
+ QueueManipulationResult,
106
128
  QueueSnapshot,
107
129
  RetryConfig,
108
- VolumeConfig
130
+ VolumeConfig,
131
+ QueueConfig
109
132
  } from './types';
110
133
 
111
134
  // Enums and constants
112
- export { EasingType, FadeType, GLOBAL_PROGRESS_KEY } from './types';
135
+ export { EasingType, FadeType, MAX_CHANNELS, TimerType, GLOBAL_PROGRESS_KEY } from './types';
package/src/info.ts CHANGED
@@ -12,16 +12,161 @@ import {
12
12
  AudioPauseCallback,
13
13
  AudioResumeCallback,
14
14
  ExtendedAudioQueueChannel,
15
- GLOBAL_PROGRESS_KEY
15
+ GLOBAL_PROGRESS_KEY,
16
+ MAX_CHANNELS
16
17
  } from './types';
17
18
  import { getAudioInfoFromElement, createQueueSnapshot } from './utils';
18
19
  import { setupProgressTracking, cleanupProgressTracking } from './events';
19
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
+
20
88
  /**
21
89
  * Global array to store audio channels with their queues and callback management
22
90
  * Each channel maintains its own audio queue and event callback sets
91
+ *
92
+ * Note: While you can inspect this array for debugging, direct modification is discouraged.
93
+ * Use the provided API functions for safe channel management.
94
+ */
95
+ export const audioChannels: ExtendedAudioQueueChannel[] = new Proxy(
96
+ [] as ExtendedAudioQueueChannel[],
97
+ {
98
+ deleteProperty(target: ExtendedAudioQueueChannel[], prop: string | symbol): boolean {
99
+ if (typeof prop === 'string' && !isNaN(Number(prop))) {
100
+ // eslint-disable-next-line no-console
101
+ console.warn(
102
+ 'Warning: Direct deletion from audioChannels detected. ' +
103
+ 'Consider using stopAllAudioInChannel() for proper cleanup.'
104
+ );
105
+ }
106
+ delete (target as unknown as Record<string, unknown>)[prop as string];
107
+ return true;
108
+ },
109
+ get(target: ExtendedAudioQueueChannel[], prop: string | symbol): unknown {
110
+ const value = (target as unknown as Record<string, unknown>)[prop as string];
111
+
112
+ // Return channel objects with warnings on modification attempts
113
+ if (
114
+ typeof value === 'object' &&
115
+ value !== null &&
116
+ typeof prop === 'string' &&
117
+ !isNaN(Number(prop))
118
+ ) {
119
+ return new Proxy(value as ExtendedAudioQueueChannel, {
120
+ set(
121
+ channelTarget: ExtendedAudioQueueChannel,
122
+ channelProp: string | symbol,
123
+ channelValue: unknown
124
+ ): boolean {
125
+ // Allow internal modifications but warn about direct property changes
126
+ // Use the automatically-derived whitelist from the interface
127
+ const whitelistedProperties = getWhitelistedChannelProperties();
128
+
129
+ if (typeof channelProp === 'string' && !whitelistedProperties.includes(channelProp)) {
130
+ // eslint-disable-next-line no-console
131
+ console.warn(
132
+ `Warning: Direct modification of channel.${channelProp} detected. ` +
133
+ 'Use API functions for safer channel management.'
134
+ );
135
+ }
136
+ const key = typeof channelProp === 'symbol' ? channelProp.toString() : channelProp;
137
+ (channelTarget as unknown as Record<string, unknown>)[key] = channelValue;
138
+ return true;
139
+ }
140
+ });
141
+ }
142
+
143
+ return value;
144
+ },
145
+ set(target: ExtendedAudioQueueChannel[], prop: string | symbol, value: unknown): boolean {
146
+ // Allow normal array operations
147
+ const key = typeof prop === 'symbol' ? prop.toString() : prop;
148
+ (target as unknown as Record<string, unknown>)[key] = value;
149
+ return true;
150
+ }
151
+ }
152
+ );
153
+
154
+ /**
155
+ * Validates a channel number against MAX_CHANNELS limit
156
+ * @param channelNumber - The channel number to validate
157
+ * @throws Error if the channel number is invalid
158
+ * @internal
23
159
  */
24
- export const audioChannels: ExtendedAudioQueueChannel[] = [];
160
+ const validateChannelNumber = (channelNumber: number): void => {
161
+ if (channelNumber < 0) {
162
+ throw new Error('Channel number must be non-negative');
163
+ }
164
+ if (channelNumber >= MAX_CHANNELS) {
165
+ throw new Error(
166
+ `Channel number ${channelNumber} exceeds maximum allowed channels (${MAX_CHANNELS})`
167
+ );
168
+ }
169
+ };
25
170
 
26
171
  /**
27
172
  * Gets current audio information for a specific channel
@@ -71,18 +216,19 @@ export const getAllChannelsInfo = (): (AudioInfo | null)[] => {
71
216
 
72
217
  /**
73
218
  * Gets a complete snapshot of the queue state for a specific channel
74
- * @param channelNumber - The channel number
219
+ * @param channelNumber - The channel number (defaults to 0)
75
220
  * @returns QueueSnapshot object or null if channel doesn't exist
76
221
  * @example
77
222
  * ```typescript
78
- * const snapshot = getQueueSnapshot(0);
223
+ * const snapshot = getQueueSnapshot();
79
224
  * if (snapshot) {
80
225
  * console.log(`Queue has ${snapshot.totalItems} items`);
81
226
  * console.log(`Currently playing: ${snapshot.items[0]?.fileName}`);
82
227
  * }
228
+ * const channelSnapshot = getQueueSnapshot(2);
83
229
  * ```
84
230
  */
85
- export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null => {
231
+ export const getQueueSnapshot = (channelNumber: number = 0): QueueSnapshot | null => {
86
232
  return createQueueSnapshot(channelNumber, audioChannels);
87
233
  };
88
234
 
@@ -90,6 +236,7 @@ export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null =>
90
236
  * Subscribes to real-time progress updates for a specific channel
91
237
  * @param channelNumber - The channel number
92
238
  * @param callback - Function to call with audio info updates
239
+ * @throws Error if the channel number exceeds the maximum allowed channels
93
240
  * @example
94
241
  * ```typescript
95
242
  * onAudioProgress(0, (info) => {
@@ -99,6 +246,8 @@ export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null =>
99
246
  * ```
100
247
  */
101
248
  export const onAudioProgress = (channelNumber: number, callback: ProgressCallback): void => {
249
+ validateChannelNumber(channelNumber);
250
+
102
251
  if (!audioChannels[channelNumber]) {
103
252
  audioChannels[channelNumber] = {
104
253
  audioCompleteCallbacks: new Set(),
@@ -140,13 +289,14 @@ export const onAudioProgress = (channelNumber: number, callback: ProgressCallbac
140
289
 
141
290
  /**
142
291
  * Removes progress listeners for a specific channel
143
- * @param channelNumber - The channel number
292
+ * @param channelNumber - The channel number (defaults to 0)
144
293
  * @example
145
294
  * ```typescript
146
- * offAudioProgress(0); // Stop receiving progress updates for channel 0
295
+ * offAudioProgress();
296
+ * offAudioProgress(1); // Stop receiving progress updates for channel 1
147
297
  * ```
148
298
  */
149
- export const offAudioProgress = (channelNumber: number): void => {
299
+ export function offAudioProgress(channelNumber: number = 0): void {
150
300
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
151
301
  if (!channel?.progressCallbacks) return;
152
302
 
@@ -158,12 +308,13 @@ export const offAudioProgress = (channelNumber: number): void => {
158
308
 
159
309
  // Clear all callbacks for this channel
160
310
  channel.progressCallbacks.clear();
161
- };
311
+ }
162
312
 
163
313
  /**
164
314
  * Subscribes to queue change events for a specific channel
165
315
  * @param channelNumber - The channel number to monitor
166
316
  * @param callback - Function to call when queue changes
317
+ * @throws Error if the channel number exceeds the maximum allowed channels
167
318
  * @example
168
319
  * ```typescript
169
320
  * onQueueChange(0, (snapshot) => {
@@ -173,6 +324,8 @@ export const offAudioProgress = (channelNumber: number): void => {
173
324
  * ```
174
325
  */
175
326
  export const onQueueChange = (channelNumber: number, callback: QueueChangeCallback): void => {
327
+ validateChannelNumber(channelNumber);
328
+
176
329
  if (!audioChannels[channelNumber]) {
177
330
  audioChannels[channelNumber] = {
178
331
  audioCompleteCallbacks: new Set(),
@@ -215,6 +368,7 @@ export const offQueueChange = (channelNumber: number): void => {
215
368
  * Subscribes to audio start events for a specific channel
216
369
  * @param channelNumber - The channel number to monitor
217
370
  * @param callback - Function to call when audio starts playing
371
+ * @throws Error if the channel number exceeds the maximum allowed channels
218
372
  * @example
219
373
  * ```typescript
220
374
  * onAudioStart(0, (info) => {
@@ -224,6 +378,8 @@ export const offQueueChange = (channelNumber: number): void => {
224
378
  * ```
225
379
  */
226
380
  export const onAudioStart = (channelNumber: number, callback: AudioStartCallback): void => {
381
+ validateChannelNumber(channelNumber);
382
+
227
383
  if (!audioChannels[channelNumber]) {
228
384
  audioChannels[channelNumber] = {
229
385
  audioCompleteCallbacks: new Set(),
@@ -251,6 +407,7 @@ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback
251
407
  * Subscribes to audio complete events for a specific channel
252
408
  * @param channelNumber - The channel number to monitor
253
409
  * @param callback - Function to call when audio completes
410
+ * @throws Error if the channel number exceeds the maximum allowed channels
254
411
  * @example
255
412
  * ```typescript
256
413
  * onAudioComplete(0, (info) => {
@@ -262,6 +419,8 @@ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback
262
419
  * ```
263
420
  */
264
421
  export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCallback): void => {
422
+ validateChannelNumber(channelNumber);
423
+
265
424
  if (!audioChannels[channelNumber]) {
266
425
  audioChannels[channelNumber] = {
267
426
  audioCompleteCallbacks: new Set(),
@@ -289,6 +448,7 @@ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCa
289
448
  * Subscribes to audio pause events for a specific channel
290
449
  * @param channelNumber - The channel number to monitor
291
450
  * @param callback - Function to call when audio is paused
451
+ * @throws Error if the channel number exceeds the maximum allowed channels
292
452
  * @example
293
453
  * ```typescript
294
454
  * onAudioPause(0, (channelNumber, info) => {
@@ -298,6 +458,8 @@ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCa
298
458
  * ```
299
459
  */
300
460
  export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback): void => {
461
+ validateChannelNumber(channelNumber);
462
+
301
463
  if (!audioChannels[channelNumber]) {
302
464
  audioChannels[channelNumber] = {
303
465
  audioCompleteCallbacks: new Set(),
@@ -325,6 +487,7 @@ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback
325
487
  * Subscribes to audio resume events for a specific channel
326
488
  * @param channelNumber - The channel number to monitor
327
489
  * @param callback - Function to call when audio is resumed
490
+ * @throws Error if the channel number exceeds the maximum allowed channels
328
491
  * @example
329
492
  * ```typescript
330
493
  * onAudioResume(0, (channelNumber, info) => {
@@ -334,6 +497,8 @@ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback
334
497
  * ```
335
498
  */
336
499
  export const onAudioResume = (channelNumber: number, callback: AudioResumeCallback): void => {
500
+ validateChannelNumber(channelNumber);
501
+
337
502
  if (!audioChannels[channelNumber]) {
338
503
  audioChannels[channelNumber] = {
339
504
  audioCompleteCallbacks: new Set(),
@@ -386,3 +551,33 @@ export const offAudioResume = (channelNumber: number): void => {
386
551
 
387
552
  channel.audioResumeCallbacks.clear();
388
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
+ };
package/src/pause.ts CHANGED
@@ -14,8 +14,6 @@ import { getAudioInfoFromElement } from './utils';
14
14
  import { emitAudioPause, emitAudioResume } from './events';
15
15
  import { transitionVolume, getFadeConfig } from './volume';
16
16
 
17
-
18
-
19
17
  /**
20
18
  * Gets the current volume for a channel, accounting for synchronous state
21
19
  * @param channelNumber - The channel number
@@ -81,7 +79,7 @@ export const pauseWithFade = async (
81
79
  // First fade or no transition in progress, capture current volume
82
80
  // But ensure we don't capture a volume of 0 during a transition
83
81
  const currentVolume = getChannelVolumeSync(channelNumber);
84
- originalVolume = currentVolume > 0 ? currentVolume : channel.fadeState?.originalVolume ?? 1.0;
82
+ originalVolume = currentVolume > 0 ? currentVolume : (channel.fadeState?.originalVolume ?? 1.0);
85
83
  }
86
84
 
87
85
  // Store fade state for resumeWithFade to use (including custom duration)
@@ -111,7 +109,7 @@ export const pauseWithFade = async (
111
109
 
112
110
  // Reset volume to original for resume (synchronously to avoid state issues)
113
111
  setChannelVolumeSync(channelNumber, originalVolume);
114
-
112
+
115
113
  // Mark transition as complete
116
114
  if (channel.fadeState) {
117
115
  channel.fadeState.isTransitioning = false;
@@ -258,11 +256,11 @@ export const pauseAllWithFade = async (
258
256
  */
259
257
  export const resumeAllWithFade = async (fadeType?: FadeType, duration?: number): Promise<void> => {
260
258
  const resumePromises: Promise<void>[] = [];
261
-
259
+
262
260
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
263
261
  resumePromises.push(resumeWithFade(fadeType, index, duration));
264
262
  });
265
-
263
+
266
264
  await Promise.all(resumePromises);
267
265
  };
268
266