audioq 2.0.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/info.ts ADDED
@@ -0,0 +1,590 @@
1
+ /**
2
+ * @fileoverview Audio information and progress tracking functions for the audioq package
3
+ */
4
+
5
+ import {
6
+ AudioInfo,
7
+ QueueSnapshot,
8
+ ProgressCallback,
9
+ QueueChangeCallback,
10
+ AudioStartCallback,
11
+ AudioCompleteCallback,
12
+ AudioPauseCallback,
13
+ AudioResumeCallback,
14
+ ExtendedAudioQueueChannel,
15
+ GLOBAL_PROGRESS_KEY,
16
+ MAX_CHANNELS
17
+ } from './types';
18
+ import { getAudioInfoFromElement, createQueueSnapshot } from './utils';
19
+ import { setupProgressTracking, cleanupProgressTracking } from './events';
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
+ 'webAudioContext', // Web Audio API context
52
+ 'webAudioNodes' // Web Audio API nodes map
53
+ ];
54
+
55
+ return [...new Set(propertyNames)]; // Remove duplicates
56
+ };
57
+
58
+ /**
59
+ * Returns the list of non-whitelisted properties found on a specific channel
60
+ * These are properties that will trigger warnings when modified directly
61
+ * @param channelNumber - The channel number to inspect (defaults to 0)
62
+ * @returns Array of property names that are not in the whitelist, or empty array if channel doesn't exist
63
+ * @example
64
+ * ```typescript
65
+ * // Add some custom property to a channel
66
+ * (audioChannels[0] as any).customProperty = 'test';
67
+ *
68
+ * const nonWhitelisted = getNonWhitelistedChannelProperties(0);
69
+ * console.log(nonWhitelisted); // ['customProperty']
70
+ * ```
71
+ * @internal
72
+ */
73
+ export const getNonWhitelistedChannelProperties = (channelNumber: number = 0): string[] => {
74
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
75
+ if (!channel) {
76
+ return [];
77
+ }
78
+
79
+ const whitelistedProperties: string[] = getWhitelistedChannelProperties();
80
+ const allChannelProperties: string[] = Object.getOwnPropertyNames(channel);
81
+
82
+ // Filter out properties that are in the whitelist
83
+ const nonWhitelistedProperties: string[] = allChannelProperties.filter(
84
+ (property: string) => !whitelistedProperties.includes(property)
85
+ );
86
+
87
+ return nonWhitelistedProperties;
88
+ };
89
+
90
+ /**
91
+ * Global array to store audio channels with their queues and callback management
92
+ * Each channel maintains its own audio queue and event callback sets
93
+ *
94
+ * Note: While you can inspect this array for debugging, direct modification is discouraged.
95
+ * Use the provided API functions for safe channel management.
96
+ */
97
+ export const audioChannels: ExtendedAudioQueueChannel[] = new Proxy(
98
+ [] as ExtendedAudioQueueChannel[],
99
+ {
100
+ deleteProperty(target: ExtendedAudioQueueChannel[], prop: string | symbol): boolean {
101
+ if (typeof prop === 'string' && !isNaN(Number(prop))) {
102
+ // eslint-disable-next-line no-console
103
+ console.warn(
104
+ 'Warning: Direct deletion from audioChannels detected. ' +
105
+ 'Consider using stopAllAudioInChannel() for proper cleanup.'
106
+ );
107
+ }
108
+ delete (target as unknown as Record<string, unknown>)[prop as string];
109
+ return true;
110
+ },
111
+ get(target: ExtendedAudioQueueChannel[], prop: string | symbol): unknown {
112
+ const value = (target as unknown as Record<string, unknown>)[prop as string];
113
+
114
+ // Return channel objects with warnings on modification attempts
115
+ if (
116
+ typeof value === 'object' &&
117
+ value !== null &&
118
+ typeof prop === 'string' &&
119
+ !isNaN(Number(prop))
120
+ ) {
121
+ return new Proxy(value as ExtendedAudioQueueChannel, {
122
+ set(
123
+ channelTarget: ExtendedAudioQueueChannel,
124
+ channelProp: string | symbol,
125
+ channelValue: unknown
126
+ ): boolean {
127
+ // Allow internal modifications but warn about direct property changes
128
+ // Use the automatically-derived whitelist from the interface
129
+ const whitelistedProperties = getWhitelistedChannelProperties();
130
+
131
+ if (typeof channelProp === 'string' && !whitelistedProperties.includes(channelProp)) {
132
+ // eslint-disable-next-line no-console
133
+ console.warn(
134
+ `Warning: Direct modification of channel.${channelProp} detected. ` +
135
+ 'Use API functions for safer channel management.'
136
+ );
137
+ }
138
+ const key = typeof channelProp === 'symbol' ? channelProp.toString() : channelProp;
139
+ (channelTarget as unknown as Record<string, unknown>)[key] = channelValue;
140
+ return true;
141
+ }
142
+ });
143
+ }
144
+
145
+ return value;
146
+ },
147
+ set(target: ExtendedAudioQueueChannel[], prop: string | symbol, value: unknown): boolean {
148
+ // Allow normal array operations
149
+ const key = typeof prop === 'symbol' ? prop.toString() : prop;
150
+ (target as unknown as Record<string, unknown>)[key] = value;
151
+ return true;
152
+ }
153
+ }
154
+ );
155
+
156
+ /**
157
+ * Validates a channel number against MAX_CHANNELS limit
158
+ * @param channelNumber - The channel number to validate
159
+ * @throws Error if the channel number is invalid
160
+ * @internal
161
+ */
162
+ const validateChannelNumber = (channelNumber: number): void => {
163
+ if (channelNumber < 0) {
164
+ throw new Error('Channel number must be non-negative');
165
+ }
166
+ if (channelNumber >= MAX_CHANNELS) {
167
+ throw new Error(
168
+ `Channel number ${channelNumber} exceeds maximum allowed channels (${MAX_CHANNELS})`
169
+ );
170
+ }
171
+ };
172
+
173
+ /**
174
+ * Gets current audio information for a specific channel
175
+ * @param channelNumber - The channel number (defaults to 0)
176
+ * @returns AudioInfo object or null if no audio is playing
177
+ * @example
178
+ * ```typescript
179
+ * const info = getCurrentAudioInfo(0);
180
+ * if (info) {
181
+ * console.log(`Currently playing: ${info.fileName}`);
182
+ * console.log(`Progress: ${(info.progress * 100).toFixed(1)}%`);
183
+ * }
184
+ * ```
185
+ */
186
+ export const getCurrentAudioInfo = (channelNumber: number = 0): AudioInfo | null => {
187
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
188
+ if (!channel || channel.queue.length === 0) {
189
+ return null;
190
+ }
191
+
192
+ const currentAudio: HTMLAudioElement = channel.queue[0];
193
+ return getAudioInfoFromElement(currentAudio, channelNumber, audioChannels);
194
+ };
195
+
196
+ /**
197
+ * Gets audio information for all channels
198
+ * @returns Array of AudioInfo objects (null for channels with no audio)
199
+ * @example
200
+ * ```typescript
201
+ * const allInfo = getAllChannelsInfo();
202
+ * allInfo.forEach((info, channel) => {
203
+ * if (info) {
204
+ * console.log(`Channel ${channel}: ${info.fileName}`);
205
+ * }
206
+ * });
207
+ * ```
208
+ */
209
+ export const getAllChannelsInfo = (): (AudioInfo | null)[] => {
210
+ const allChannelsInfo: (AudioInfo | null)[] = [];
211
+
212
+ for (let i: number = 0; i < audioChannels.length; i++) {
213
+ allChannelsInfo.push(getCurrentAudioInfo(i));
214
+ }
215
+
216
+ return allChannelsInfo;
217
+ };
218
+
219
+ /**
220
+ * Gets a complete snapshot of the queue state for a specific channel
221
+ * @param channelNumber - The channel number (defaults to 0)
222
+ * @returns QueueSnapshot object or null if channel doesn't exist
223
+ * @example
224
+ * ```typescript
225
+ * const snapshot = getQueueSnapshot();
226
+ * if (snapshot) {
227
+ * console.log(`Queue has ${snapshot.totalItems} items`);
228
+ * console.log(`Currently playing: ${snapshot.items[0]?.fileName}`);
229
+ * }
230
+ * const channelSnapshot = getQueueSnapshot(2);
231
+ * ```
232
+ */
233
+ export const getQueueSnapshot = (channelNumber: number = 0): QueueSnapshot | null => {
234
+ return createQueueSnapshot(channelNumber, audioChannels);
235
+ };
236
+
237
+ /**
238
+ * Subscribes to real-time progress updates for a specific channel
239
+ * @param channelNumber - The channel number
240
+ * @param callback - Function to call with audio info updates
241
+ * @throws Error if the channel number exceeds the maximum allowed channels
242
+ * @example
243
+ * ```typescript
244
+ * onAudioProgress(0, (info) => {
245
+ * updateProgressBar(info.progress);
246
+ * updateTimeDisplay(info.currentTime, info.duration);
247
+ * });
248
+ * ```
249
+ */
250
+ export const onAudioProgress = (channelNumber: number, callback: ProgressCallback): void => {
251
+ validateChannelNumber(channelNumber);
252
+
253
+ if (!audioChannels[channelNumber]) {
254
+ audioChannels[channelNumber] = {
255
+ audioCompleteCallbacks: new Set(),
256
+ audioErrorCallbacks: new Set(),
257
+ audioPauseCallbacks: new Set(),
258
+ audioResumeCallbacks: new Set(),
259
+ audioStartCallbacks: new Set(),
260
+ isPaused: false,
261
+ progressCallbacks: new Map(),
262
+ queue: [],
263
+ queueChangeCallbacks: new Set(),
264
+ volume: 1.0
265
+ };
266
+ }
267
+
268
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
269
+ if (!channel.progressCallbacks) {
270
+ channel.progressCallbacks = new Map();
271
+ }
272
+
273
+ // Add callback for current audio if exists
274
+ if (channel.queue.length > 0) {
275
+ const currentAudio: HTMLAudioElement = channel.queue[0];
276
+ if (!channel.progressCallbacks.has(currentAudio)) {
277
+ channel.progressCallbacks.set(currentAudio, new Set());
278
+ }
279
+ channel.progressCallbacks.get(currentAudio)!.add(callback);
280
+
281
+ // Set up tracking if not already done
282
+ setupProgressTracking(currentAudio, channelNumber, audioChannels);
283
+ }
284
+
285
+ // Store callback for future audio elements in this channel
286
+ if (!channel.progressCallbacks.has(GLOBAL_PROGRESS_KEY)) {
287
+ channel.progressCallbacks.set(GLOBAL_PROGRESS_KEY, new Set());
288
+ }
289
+ channel.progressCallbacks.get(GLOBAL_PROGRESS_KEY)!.add(callback);
290
+ };
291
+
292
+ /**
293
+ * Removes progress listeners for a specific channel
294
+ * @param channelNumber - The channel number (defaults to 0)
295
+ * @example
296
+ * ```typescript
297
+ * offAudioProgress();
298
+ * offAudioProgress(1); // Stop receiving progress updates for channel 1
299
+ * ```
300
+ */
301
+ export function offAudioProgress(channelNumber: number = 0): void {
302
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
303
+ if (!channel?.progressCallbacks) return;
304
+
305
+ // Clean up event listeners for current audio if exists
306
+ if (channel.queue.length > 0) {
307
+ const currentAudio: HTMLAudioElement = channel.queue[0];
308
+ cleanupProgressTracking(currentAudio, channelNumber, audioChannels);
309
+ }
310
+
311
+ // Clear all callbacks for this channel
312
+ channel.progressCallbacks.clear();
313
+ }
314
+
315
+ /**
316
+ * Subscribes to queue change events for a specific channel
317
+ * @param channelNumber - The channel number to monitor
318
+ * @param callback - Function to call when queue changes
319
+ * @throws Error if the channel number exceeds the maximum allowed channels
320
+ * @example
321
+ * ```typescript
322
+ * onQueueChange(0, (snapshot) => {
323
+ * updateQueueDisplay(snapshot.items);
324
+ * updateQueueCount(snapshot.totalItems);
325
+ * });
326
+ * ```
327
+ */
328
+ export const onQueueChange = (channelNumber: number, callback: QueueChangeCallback): void => {
329
+ validateChannelNumber(channelNumber);
330
+
331
+ if (!audioChannels[channelNumber]) {
332
+ audioChannels[channelNumber] = {
333
+ audioCompleteCallbacks: new Set(),
334
+ audioErrorCallbacks: new Set(),
335
+ audioPauseCallbacks: new Set(),
336
+ audioResumeCallbacks: new Set(),
337
+ audioStartCallbacks: new Set(),
338
+ isPaused: false,
339
+ progressCallbacks: new Map(),
340
+ queue: [],
341
+ queueChangeCallbacks: new Set(),
342
+ volume: 1.0
343
+ };
344
+ }
345
+
346
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
347
+ if (!channel.queueChangeCallbacks) {
348
+ channel.queueChangeCallbacks = new Set();
349
+ }
350
+
351
+ channel.queueChangeCallbacks.add(callback);
352
+ };
353
+
354
+ /**
355
+ * Removes queue change listeners for a specific channel
356
+ * @param channelNumber - The channel number (defaults to 0)
357
+ * @example
358
+ * ```typescript
359
+ * offQueueChange(); // Stop receiving queue change notifications for default channel (0)
360
+ * offQueueChange(1); // Stop receiving queue change notifications for channel 1
361
+ * ```
362
+ */
363
+ export const offQueueChange = (channelNumber: number = 0): void => {
364
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
365
+ if (!channel?.queueChangeCallbacks) return;
366
+
367
+ channel.queueChangeCallbacks.clear();
368
+ };
369
+
370
+ /**
371
+ * Subscribes to audio start events for a specific channel
372
+ * @param channelNumber - The channel number to monitor
373
+ * @param callback - Function to call when audio starts playing
374
+ * @throws Error if the channel number exceeds the maximum allowed channels
375
+ * @example
376
+ * ```typescript
377
+ * onAudioStart(0, (info) => {
378
+ * showNowPlaying(info.fileName);
379
+ * setTotalDuration(info.duration);
380
+ * });
381
+ * ```
382
+ */
383
+ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback): void => {
384
+ validateChannelNumber(channelNumber);
385
+
386
+ if (!audioChannels[channelNumber]) {
387
+ audioChannels[channelNumber] = {
388
+ audioCompleteCallbacks: new Set(),
389
+ audioErrorCallbacks: new Set(),
390
+ audioPauseCallbacks: new Set(),
391
+ audioResumeCallbacks: new Set(),
392
+ audioStartCallbacks: new Set(),
393
+ isPaused: false,
394
+ progressCallbacks: new Map(),
395
+ queue: [],
396
+ queueChangeCallbacks: new Set(),
397
+ volume: 1.0
398
+ };
399
+ }
400
+
401
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
402
+ if (!channel.audioStartCallbacks) {
403
+ channel.audioStartCallbacks = new Set();
404
+ }
405
+
406
+ channel.audioStartCallbacks.add(callback);
407
+ };
408
+
409
+ /**
410
+ * Subscribes to audio complete events for a specific channel
411
+ * @param channelNumber - The channel number to monitor
412
+ * @param callback - Function to call when audio completes
413
+ * @throws Error if the channel number exceeds the maximum allowed channels
414
+ * @example
415
+ * ```typescript
416
+ * onAudioComplete(0, (info) => {
417
+ * logPlaybackComplete(info.fileName);
418
+ * if (info.remainingInQueue === 0) {
419
+ * showQueueComplete();
420
+ * }
421
+ * });
422
+ * ```
423
+ */
424
+ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCallback): void => {
425
+ validateChannelNumber(channelNumber);
426
+
427
+ if (!audioChannels[channelNumber]) {
428
+ audioChannels[channelNumber] = {
429
+ audioCompleteCallbacks: new Set(),
430
+ audioErrorCallbacks: new Set(),
431
+ audioPauseCallbacks: new Set(),
432
+ audioResumeCallbacks: new Set(),
433
+ audioStartCallbacks: new Set(),
434
+ isPaused: false,
435
+ progressCallbacks: new Map(),
436
+ queue: [],
437
+ queueChangeCallbacks: new Set(),
438
+ volume: 1.0
439
+ };
440
+ }
441
+
442
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
443
+ if (!channel.audioCompleteCallbacks) {
444
+ channel.audioCompleteCallbacks = new Set();
445
+ }
446
+
447
+ channel.audioCompleteCallbacks.add(callback);
448
+ };
449
+
450
+ /**
451
+ * Subscribes to audio pause events for a specific channel
452
+ * @param channelNumber - The channel number to monitor
453
+ * @param callback - Function to call when audio is paused
454
+ * @throws Error if the channel number exceeds the maximum allowed channels
455
+ * @example
456
+ * ```typescript
457
+ * onAudioPause(0, (channelNumber, info) => {
458
+ * showPauseIndicator();
459
+ * logPauseEvent(info.fileName, info.currentTime);
460
+ * });
461
+ * ```
462
+ */
463
+ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback): void => {
464
+ validateChannelNumber(channelNumber);
465
+
466
+ if (!audioChannels[channelNumber]) {
467
+ audioChannels[channelNumber] = {
468
+ audioCompleteCallbacks: new Set(),
469
+ audioErrorCallbacks: new Set(),
470
+ audioPauseCallbacks: new Set(),
471
+ audioResumeCallbacks: new Set(),
472
+ audioStartCallbacks: new Set(),
473
+ isPaused: false,
474
+ progressCallbacks: new Map(),
475
+ queue: [],
476
+ queueChangeCallbacks: new Set(),
477
+ volume: 1.0
478
+ };
479
+ }
480
+
481
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
482
+ if (!channel.audioPauseCallbacks) {
483
+ channel.audioPauseCallbacks = new Set();
484
+ }
485
+
486
+ channel.audioPauseCallbacks.add(callback);
487
+ };
488
+
489
+ /**
490
+ * Subscribes to audio resume events for a specific channel
491
+ * @param channelNumber - The channel number to monitor
492
+ * @param callback - Function to call when audio is resumed
493
+ * @throws Error if the channel number exceeds the maximum allowed channels
494
+ * @example
495
+ * ```typescript
496
+ * onAudioResume(0, (channelNumber, info) => {
497
+ * hidePauseIndicator();
498
+ * logResumeEvent(info.fileName, info.currentTime);
499
+ * });
500
+ * ```
501
+ */
502
+ export const onAudioResume = (channelNumber: number, callback: AudioResumeCallback): void => {
503
+ validateChannelNumber(channelNumber);
504
+
505
+ if (!audioChannels[channelNumber]) {
506
+ audioChannels[channelNumber] = {
507
+ audioCompleteCallbacks: new Set(),
508
+ audioErrorCallbacks: new Set(),
509
+ audioPauseCallbacks: new Set(),
510
+ audioResumeCallbacks: new Set(),
511
+ audioStartCallbacks: new Set(),
512
+ isPaused: false,
513
+ progressCallbacks: new Map(),
514
+ queue: [],
515
+ queueChangeCallbacks: new Set(),
516
+ volume: 1.0
517
+ };
518
+ }
519
+
520
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
521
+ if (!channel.audioResumeCallbacks) {
522
+ channel.audioResumeCallbacks = new Set();
523
+ }
524
+
525
+ channel.audioResumeCallbacks.add(callback);
526
+ };
527
+
528
+ /**
529
+ * Removes pause event listeners for a specific channel
530
+ * @param channelNumber - The channel number (defaults to 0)
531
+ * @example
532
+ * ```typescript
533
+ * offAudioPause(); // Stop receiving pause notifications for default channel (0)
534
+ * offAudioPause(1); // Stop receiving pause notifications for channel 1
535
+ * ```
536
+ */
537
+ export const offAudioPause = (channelNumber: number = 0): void => {
538
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
539
+ if (!channel?.audioPauseCallbacks) return;
540
+
541
+ channel.audioPauseCallbacks.clear();
542
+ };
543
+
544
+ /**
545
+ * Removes resume event listeners for a specific channel
546
+ * @param channelNumber - The channel number (defaults to 0)
547
+ * @example
548
+ * ```typescript
549
+ * offAudioResume(); // Stop receiving resume notifications for default channel (0)
550
+ * offAudioResume(1); // Stop receiving resume notifications for channel 1
551
+ * ```
552
+ */
553
+ export const offAudioResume = (channelNumber: number = 0): void => {
554
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
555
+ if (!channel?.audioResumeCallbacks) return;
556
+
557
+ channel.audioResumeCallbacks.clear();
558
+ };
559
+
560
+ /**
561
+ * Removes audio start event listeners for a specific channel
562
+ * @param channelNumber - The channel number (defaults to 0)
563
+ * @example
564
+ * ```typescript
565
+ * offAudioStart(); // Stop receiving start notifications for default channel (0)
566
+ * offAudioStart(1); // Stop receiving start notifications for channel 1
567
+ * ```
568
+ */
569
+ export const offAudioStart = (channelNumber: number = 0): void => {
570
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
571
+ if (!channel?.audioStartCallbacks) return;
572
+
573
+ channel.audioStartCallbacks.clear();
574
+ };
575
+
576
+ /**
577
+ * Removes audio complete event listeners for a specific channel
578
+ * @param channelNumber - The channel number (defaults to 0)
579
+ * @example
580
+ * ```typescript
581
+ * offAudioComplete(); // Stop receiving completion notifications for default channel (0)
582
+ * offAudioComplete(1); // Stop receiving completion notifications for channel 1
583
+ * ```
584
+ */
585
+ export const offAudioComplete = (channelNumber: number = 0): void => {
586
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
587
+ if (!channel?.audioCompleteCallbacks) return;
588
+
589
+ channel.audioCompleteCallbacks.clear();
590
+ };