audio-channel-queue 1.9.0 → 1.10.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 +197 -313
- package/dist/core.d.ts +59 -1
- package/dist/core.js +331 -40
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +10 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +21 -2
- package/dist/info.d.ts +17 -6
- package/dist/info.js +81 -10
- package/dist/pause.js +1 -1
- package/dist/queue-manipulation.d.ts +104 -0
- package/dist/queue-manipulation.js +319 -0
- package/dist/types.d.ts +46 -8
- package/dist/types.js +13 -1
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +98 -10
- package/dist/volume.d.ts +14 -1
- package/dist/volume.js +173 -54
- package/package.json +12 -2
- package/src/core.ts +389 -50
- package/src/errors.ts +14 -2
- package/src/index.ts +26 -5
- package/src/info.ts +107 -9
- package/src/pause.ts +4 -6
- package/src/queue-manipulation.ts +378 -0
- package/src/types.ts +51 -9
- package/src/utils.ts +110 -9
- package/src/volume.ts +214 -62
package/src/info.ts
CHANGED
|
@@ -12,7 +12,8 @@ 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';
|
|
@@ -20,8 +21,85 @@ import { setupProgressTracking, cleanupProgressTracking } from './events';
|
|
|
20
21
|
/**
|
|
21
22
|
* Global array to store audio channels with their queues and callback management
|
|
22
23
|
* Each channel maintains its own audio queue and event callback sets
|
|
24
|
+
*
|
|
25
|
+
* Note: While you can inspect this array for debugging, direct modification is discouraged.
|
|
26
|
+
* Use the provided API functions for safe channel management.
|
|
23
27
|
*/
|
|
24
|
-
export const audioChannels: ExtendedAudioQueueChannel[] =
|
|
28
|
+
export const audioChannels: ExtendedAudioQueueChannel[] = new Proxy(
|
|
29
|
+
[] as ExtendedAudioQueueChannel[],
|
|
30
|
+
{
|
|
31
|
+
deleteProperty(target: ExtendedAudioQueueChannel[], prop: string | symbol): boolean {
|
|
32
|
+
if (typeof prop === 'string' && !isNaN(Number(prop))) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.warn(
|
|
35
|
+
'Warning: Direct deletion from audioChannels detected. ' +
|
|
36
|
+
'Consider using stopAllAudioInChannel() for proper cleanup.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
delete (target as unknown as Record<string, unknown>)[prop as string];
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
get(target: ExtendedAudioQueueChannel[], prop: string | symbol): unknown {
|
|
43
|
+
const value = (target as unknown as Record<string, unknown>)[prop as string];
|
|
44
|
+
|
|
45
|
+
// Return channel objects with warnings on modification attempts
|
|
46
|
+
if (
|
|
47
|
+
typeof value === 'object' &&
|
|
48
|
+
value !== null &&
|
|
49
|
+
typeof prop === 'string' &&
|
|
50
|
+
!isNaN(Number(prop))
|
|
51
|
+
) {
|
|
52
|
+
return new Proxy(value as ExtendedAudioQueueChannel, {
|
|
53
|
+
set(
|
|
54
|
+
channelTarget: ExtendedAudioQueueChannel,
|
|
55
|
+
channelProp: string | symbol,
|
|
56
|
+
channelValue: unknown
|
|
57
|
+
): boolean {
|
|
58
|
+
// Allow internal modifications but warn about direct property changes
|
|
59
|
+
if (
|
|
60
|
+
typeof channelProp === 'string' &&
|
|
61
|
+
!['queue', 'volume', 'isPaused', 'isLocked', 'volumeConfig'].includes(channelProp)
|
|
62
|
+
) {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(
|
|
65
|
+
`Warning: Direct modification of channel.${channelProp} detected. ` +
|
|
66
|
+
'Use API functions for safer channel management.'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const key = typeof channelProp === 'symbol' ? channelProp.toString() : channelProp;
|
|
70
|
+
(channelTarget as unknown as Record<string, unknown>)[key] = channelValue;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return value;
|
|
77
|
+
},
|
|
78
|
+
set(target: ExtendedAudioQueueChannel[], prop: string | symbol, value: unknown): boolean {
|
|
79
|
+
// Allow normal array operations
|
|
80
|
+
const key = typeof prop === 'symbol' ? prop.toString() : prop;
|
|
81
|
+
(target as unknown as Record<string, unknown>)[key] = value;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates a channel number against MAX_CHANNELS limit
|
|
89
|
+
* @param channelNumber - The channel number to validate
|
|
90
|
+
* @throws Error if the channel number is invalid
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
const validateChannelNumber = (channelNumber: number): void => {
|
|
94
|
+
if (channelNumber < 0) {
|
|
95
|
+
throw new Error('Channel number must be non-negative');
|
|
96
|
+
}
|
|
97
|
+
if (channelNumber >= MAX_CHANNELS) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Channel number ${channelNumber} exceeds maximum allowed channels (${MAX_CHANNELS})`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
25
103
|
|
|
26
104
|
/**
|
|
27
105
|
* Gets current audio information for a specific channel
|
|
@@ -71,18 +149,19 @@ export const getAllChannelsInfo = (): (AudioInfo | null)[] => {
|
|
|
71
149
|
|
|
72
150
|
/**
|
|
73
151
|
* Gets a complete snapshot of the queue state for a specific channel
|
|
74
|
-
* @param channelNumber - The channel number
|
|
152
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
75
153
|
* @returns QueueSnapshot object or null if channel doesn't exist
|
|
76
154
|
* @example
|
|
77
155
|
* ```typescript
|
|
78
|
-
* const snapshot = getQueueSnapshot(
|
|
156
|
+
* const snapshot = getQueueSnapshot();
|
|
79
157
|
* if (snapshot) {
|
|
80
158
|
* console.log(`Queue has ${snapshot.totalItems} items`);
|
|
81
159
|
* console.log(`Currently playing: ${snapshot.items[0]?.fileName}`);
|
|
82
160
|
* }
|
|
161
|
+
* const channelSnapshot = getQueueSnapshot(2);
|
|
83
162
|
* ```
|
|
84
163
|
*/
|
|
85
|
-
export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null => {
|
|
164
|
+
export const getQueueSnapshot = (channelNumber: number = 0): QueueSnapshot | null => {
|
|
86
165
|
return createQueueSnapshot(channelNumber, audioChannels);
|
|
87
166
|
};
|
|
88
167
|
|
|
@@ -90,6 +169,7 @@ export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null =>
|
|
|
90
169
|
* Subscribes to real-time progress updates for a specific channel
|
|
91
170
|
* @param channelNumber - The channel number
|
|
92
171
|
* @param callback - Function to call with audio info updates
|
|
172
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
93
173
|
* @example
|
|
94
174
|
* ```typescript
|
|
95
175
|
* onAudioProgress(0, (info) => {
|
|
@@ -99,6 +179,8 @@ export const getQueueSnapshot = (channelNumber: number): QueueSnapshot | null =>
|
|
|
99
179
|
* ```
|
|
100
180
|
*/
|
|
101
181
|
export const onAudioProgress = (channelNumber: number, callback: ProgressCallback): void => {
|
|
182
|
+
validateChannelNumber(channelNumber);
|
|
183
|
+
|
|
102
184
|
if (!audioChannels[channelNumber]) {
|
|
103
185
|
audioChannels[channelNumber] = {
|
|
104
186
|
audioCompleteCallbacks: new Set(),
|
|
@@ -140,13 +222,14 @@ export const onAudioProgress = (channelNumber: number, callback: ProgressCallbac
|
|
|
140
222
|
|
|
141
223
|
/**
|
|
142
224
|
* Removes progress listeners for a specific channel
|
|
143
|
-
* @param channelNumber - The channel number
|
|
225
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
144
226
|
* @example
|
|
145
227
|
* ```typescript
|
|
146
|
-
* offAudioProgress(
|
|
228
|
+
* offAudioProgress();
|
|
229
|
+
* offAudioProgress(1); // Stop receiving progress updates for channel 1
|
|
147
230
|
* ```
|
|
148
231
|
*/
|
|
149
|
-
export
|
|
232
|
+
export function offAudioProgress(channelNumber: number = 0): void {
|
|
150
233
|
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
151
234
|
if (!channel?.progressCallbacks) return;
|
|
152
235
|
|
|
@@ -158,12 +241,13 @@ export const offAudioProgress = (channelNumber: number): void => {
|
|
|
158
241
|
|
|
159
242
|
// Clear all callbacks for this channel
|
|
160
243
|
channel.progressCallbacks.clear();
|
|
161
|
-
}
|
|
244
|
+
}
|
|
162
245
|
|
|
163
246
|
/**
|
|
164
247
|
* Subscribes to queue change events for a specific channel
|
|
165
248
|
* @param channelNumber - The channel number to monitor
|
|
166
249
|
* @param callback - Function to call when queue changes
|
|
250
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
167
251
|
* @example
|
|
168
252
|
* ```typescript
|
|
169
253
|
* onQueueChange(0, (snapshot) => {
|
|
@@ -173,6 +257,8 @@ export const offAudioProgress = (channelNumber: number): void => {
|
|
|
173
257
|
* ```
|
|
174
258
|
*/
|
|
175
259
|
export const onQueueChange = (channelNumber: number, callback: QueueChangeCallback): void => {
|
|
260
|
+
validateChannelNumber(channelNumber);
|
|
261
|
+
|
|
176
262
|
if (!audioChannels[channelNumber]) {
|
|
177
263
|
audioChannels[channelNumber] = {
|
|
178
264
|
audioCompleteCallbacks: new Set(),
|
|
@@ -215,6 +301,7 @@ export const offQueueChange = (channelNumber: number): void => {
|
|
|
215
301
|
* Subscribes to audio start events for a specific channel
|
|
216
302
|
* @param channelNumber - The channel number to monitor
|
|
217
303
|
* @param callback - Function to call when audio starts playing
|
|
304
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
218
305
|
* @example
|
|
219
306
|
* ```typescript
|
|
220
307
|
* onAudioStart(0, (info) => {
|
|
@@ -224,6 +311,8 @@ export const offQueueChange = (channelNumber: number): void => {
|
|
|
224
311
|
* ```
|
|
225
312
|
*/
|
|
226
313
|
export const onAudioStart = (channelNumber: number, callback: AudioStartCallback): void => {
|
|
314
|
+
validateChannelNumber(channelNumber);
|
|
315
|
+
|
|
227
316
|
if (!audioChannels[channelNumber]) {
|
|
228
317
|
audioChannels[channelNumber] = {
|
|
229
318
|
audioCompleteCallbacks: new Set(),
|
|
@@ -251,6 +340,7 @@ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback
|
|
|
251
340
|
* Subscribes to audio complete events for a specific channel
|
|
252
341
|
* @param channelNumber - The channel number to monitor
|
|
253
342
|
* @param callback - Function to call when audio completes
|
|
343
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
254
344
|
* @example
|
|
255
345
|
* ```typescript
|
|
256
346
|
* onAudioComplete(0, (info) => {
|
|
@@ -262,6 +352,8 @@ export const onAudioStart = (channelNumber: number, callback: AudioStartCallback
|
|
|
262
352
|
* ```
|
|
263
353
|
*/
|
|
264
354
|
export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCallback): void => {
|
|
355
|
+
validateChannelNumber(channelNumber);
|
|
356
|
+
|
|
265
357
|
if (!audioChannels[channelNumber]) {
|
|
266
358
|
audioChannels[channelNumber] = {
|
|
267
359
|
audioCompleteCallbacks: new Set(),
|
|
@@ -289,6 +381,7 @@ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCa
|
|
|
289
381
|
* Subscribes to audio pause events for a specific channel
|
|
290
382
|
* @param channelNumber - The channel number to monitor
|
|
291
383
|
* @param callback - Function to call when audio is paused
|
|
384
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
292
385
|
* @example
|
|
293
386
|
* ```typescript
|
|
294
387
|
* onAudioPause(0, (channelNumber, info) => {
|
|
@@ -298,6 +391,8 @@ export const onAudioComplete = (channelNumber: number, callback: AudioCompleteCa
|
|
|
298
391
|
* ```
|
|
299
392
|
*/
|
|
300
393
|
export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback): void => {
|
|
394
|
+
validateChannelNumber(channelNumber);
|
|
395
|
+
|
|
301
396
|
if (!audioChannels[channelNumber]) {
|
|
302
397
|
audioChannels[channelNumber] = {
|
|
303
398
|
audioCompleteCallbacks: new Set(),
|
|
@@ -325,6 +420,7 @@ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback
|
|
|
325
420
|
* Subscribes to audio resume events for a specific channel
|
|
326
421
|
* @param channelNumber - The channel number to monitor
|
|
327
422
|
* @param callback - Function to call when audio is resumed
|
|
423
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
328
424
|
* @example
|
|
329
425
|
* ```typescript
|
|
330
426
|
* onAudioResume(0, (channelNumber, info) => {
|
|
@@ -334,6 +430,8 @@ export const onAudioPause = (channelNumber: number, callback: AudioPauseCallback
|
|
|
334
430
|
* ```
|
|
335
431
|
*/
|
|
336
432
|
export const onAudioResume = (channelNumber: number, callback: AudioResumeCallback): void => {
|
|
433
|
+
validateChannelNumber(channelNumber);
|
|
434
|
+
|
|
337
435
|
if (!audioChannels[channelNumber]) {
|
|
338
436
|
audioChannels[channelNumber] = {
|
|
339
437
|
audioCompleteCallbacks: new Set(),
|
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
|
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Queue manipulation functions for the audio-channel-queue package
|
|
3
|
+
* Provides advanced queue management including item removal, reordering, and clearing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ExtendedAudioQueueChannel,
|
|
8
|
+
QueueManipulationResult,
|
|
9
|
+
QueueSnapshot,
|
|
10
|
+
QueueItem
|
|
11
|
+
} from './types';
|
|
12
|
+
import { audioChannels } from './info';
|
|
13
|
+
import { emitQueueChange } from './events';
|
|
14
|
+
import { cleanupProgressTracking } from './events';
|
|
15
|
+
import { createQueueSnapshot, getAudioInfoFromElement } from './utils';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Removes a specific item from the queue by its slot number (0-based index)
|
|
19
|
+
* Cannot remove the currently playing item (index 0) - use stopCurrentAudioInChannel instead
|
|
20
|
+
* @param queuedSlotNumber - Zero-based index of the item to remove (must be > 0)
|
|
21
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
22
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
23
|
+
* @throws Error if trying to remove currently playing item or invalid slot number
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // Remove the second item in queue (index 1)
|
|
27
|
+
* const result = await removeQueuedItem(1, 0);
|
|
28
|
+
* if (result.success) {
|
|
29
|
+
* console.log(`Removed item, queue now has ${result.updatedQueue.totalItems} items`);
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* // Remove the third item from channel 1
|
|
33
|
+
* await removeQueuedItem(2, 1);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const removeQueuedItem = async (
|
|
37
|
+
queuedSlotNumber: number,
|
|
38
|
+
channelNumber: number = 0
|
|
39
|
+
): Promise<QueueManipulationResult> => {
|
|
40
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
41
|
+
|
|
42
|
+
if (!channel) {
|
|
43
|
+
return {
|
|
44
|
+
error: `Channel ${channelNumber} does not exist`,
|
|
45
|
+
success: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (queuedSlotNumber < 0 || queuedSlotNumber >= channel.queue.length) {
|
|
50
|
+
return {
|
|
51
|
+
error:
|
|
52
|
+
`Invalid slot number ${queuedSlotNumber}. Queue has ${channel.queue.length} items ` +
|
|
53
|
+
`(indices 0-${channel.queue.length - 1})`,
|
|
54
|
+
success: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (queuedSlotNumber === 0) {
|
|
59
|
+
return {
|
|
60
|
+
error:
|
|
61
|
+
'Cannot remove currently playing item (index 0). ' +
|
|
62
|
+
'Use stopCurrentAudioInChannel() instead',
|
|
63
|
+
success: false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Remove the audio element from the queue
|
|
68
|
+
const removedAudio: HTMLAudioElement = channel.queue.splice(queuedSlotNumber, 1)[0];
|
|
69
|
+
|
|
70
|
+
// Clean up any progress tracking for the removed audio
|
|
71
|
+
cleanupProgressTracking(removedAudio, channelNumber, audioChannels);
|
|
72
|
+
|
|
73
|
+
// Emit queue change event
|
|
74
|
+
emitQueueChange(channelNumber, audioChannels);
|
|
75
|
+
|
|
76
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
updatedQueue: updatedQueue ?? undefined
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reorders a queue item by moving it from one position to another
|
|
86
|
+
* Cannot reorder the currently playing item (index 0)
|
|
87
|
+
* @param currentQueuedSlotNumber - Current zero-based index of the item to move (must be > 0)
|
|
88
|
+
* @param newQueuedSlotNumber - New zero-based index where the item should be placed (must be > 0)
|
|
89
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
90
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
91
|
+
* @throws Error if trying to reorder currently playing item or invalid slot numbers
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // Move item from position 2 to position 1 (make it play next)
|
|
95
|
+
* const result = await reorderQueue(2, 1, 0);
|
|
96
|
+
* if (result.success) {
|
|
97
|
+
* console.log('Item moved successfully');
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* // Move item from position 1 to end of queue
|
|
101
|
+
* await reorderQueue(1, 4, 0); // Assuming queue has 5+ items
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export const reorderQueue = async (
|
|
105
|
+
currentQueuedSlotNumber: number,
|
|
106
|
+
newQueuedSlotNumber: number,
|
|
107
|
+
channelNumber: number = 0
|
|
108
|
+
): Promise<QueueManipulationResult> => {
|
|
109
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
110
|
+
|
|
111
|
+
if (!channel) {
|
|
112
|
+
return {
|
|
113
|
+
error: `Channel ${channelNumber} does not exist`,
|
|
114
|
+
success: false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (currentQueuedSlotNumber < 0 || currentQueuedSlotNumber >= channel.queue.length) {
|
|
119
|
+
return {
|
|
120
|
+
error:
|
|
121
|
+
`Invalid current slot number ${currentQueuedSlotNumber}. Queue has ` +
|
|
122
|
+
`${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
|
|
123
|
+
success: false
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (newQueuedSlotNumber < 0 || newQueuedSlotNumber >= channel.queue.length) {
|
|
128
|
+
return {
|
|
129
|
+
error:
|
|
130
|
+
`Invalid new slot number ${newQueuedSlotNumber}. Queue has ` +
|
|
131
|
+
`${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
|
|
132
|
+
success: false
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (currentQueuedSlotNumber === 0) {
|
|
137
|
+
return {
|
|
138
|
+
error:
|
|
139
|
+
'Cannot reorder currently playing item (index 0). ' + 'Stop current audio first if needed',
|
|
140
|
+
success: false
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (newQueuedSlotNumber === 0) {
|
|
145
|
+
return {
|
|
146
|
+
error:
|
|
147
|
+
'Cannot move item to currently playing position (index 0). ' +
|
|
148
|
+
'Use queueAudioPriority() to add items to front of queue',
|
|
149
|
+
success: false
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (currentQueuedSlotNumber === newQueuedSlotNumber) {
|
|
154
|
+
// No change needed, but return success
|
|
155
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
updatedQueue: updatedQueue ?? undefined
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Remove the item from its current position
|
|
163
|
+
const audioToMove: HTMLAudioElement = channel.queue.splice(currentQueuedSlotNumber, 1)[0];
|
|
164
|
+
|
|
165
|
+
// Insert it at the new position
|
|
166
|
+
channel.queue.splice(newQueuedSlotNumber, 0, audioToMove);
|
|
167
|
+
|
|
168
|
+
// Emit queue change event
|
|
169
|
+
emitQueueChange(channelNumber, audioChannels);
|
|
170
|
+
|
|
171
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
updatedQueue: updatedQueue ?? undefined
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clears all queued audio items after the currently playing item
|
|
181
|
+
* The current audio will continue playing but nothing will follow it
|
|
182
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
183
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // Let current song finish but clear everything after it
|
|
187
|
+
* const result = await clearQueueAfterCurrent(0);
|
|
188
|
+
* if (result.success) {
|
|
189
|
+
* console.log(`Cleared queue, current audio will be the last to play`);
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export const clearQueueAfterCurrent = async (
|
|
194
|
+
channelNumber: number = 0
|
|
195
|
+
): Promise<QueueManipulationResult> => {
|
|
196
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
197
|
+
|
|
198
|
+
if (!channel) {
|
|
199
|
+
// For empty/non-existent channels, we can consider this a successful no-op
|
|
200
|
+
// since there's nothing to clear anyway
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
updatedQueue: {
|
|
204
|
+
channelNumber,
|
|
205
|
+
currentIndex: -1,
|
|
206
|
+
isPaused: false,
|
|
207
|
+
items: [],
|
|
208
|
+
totalItems: 0,
|
|
209
|
+
volume: 1.0
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (channel.queue.length <= 1) {
|
|
215
|
+
// Nothing to clear - either empty queue or only current audio
|
|
216
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
217
|
+
return {
|
|
218
|
+
success: true,
|
|
219
|
+
updatedQueue: updatedQueue ?? undefined
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Clean up progress tracking for all items except the current one
|
|
224
|
+
for (let i: number = 1; i < channel.queue.length; i++) {
|
|
225
|
+
cleanupProgressTracking(channel.queue[i], channelNumber, audioChannels);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Keep only the currently playing audio (index 0)
|
|
229
|
+
channel.queue = channel.queue.slice(0, 1);
|
|
230
|
+
|
|
231
|
+
// Emit queue change event
|
|
232
|
+
emitQueueChange(channelNumber, audioChannels);
|
|
233
|
+
|
|
234
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
success: true,
|
|
238
|
+
updatedQueue: updatedQueue ?? undefined
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Gets information about a specific queue item by its slot number
|
|
244
|
+
* @param queueSlotNumber - Zero-based index of the queue item
|
|
245
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
246
|
+
* @returns QueueItem information or null if slot doesn't exist
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* const itemInfo = getQueueItemInfo(1, 0);
|
|
250
|
+
* if (itemInfo) {
|
|
251
|
+
* console.log(`Next to play: ${itemInfo.fileName}`);
|
|
252
|
+
* console.log(`Duration: ${itemInfo.duration}ms`);
|
|
253
|
+
* }
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export const getQueueItemInfo = (
|
|
257
|
+
queueSlotNumber: number,
|
|
258
|
+
channelNumber: number = 0
|
|
259
|
+
): QueueItem | null => {
|
|
260
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
261
|
+
|
|
262
|
+
if (!channel || queueSlotNumber < 0 || queueSlotNumber >= channel.queue.length) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const audio: HTMLAudioElement = channel.queue[queueSlotNumber];
|
|
267
|
+
const audioInfo = getAudioInfoFromElement(audio, channelNumber, audioChannels);
|
|
268
|
+
|
|
269
|
+
if (!audioInfo) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { duration, fileName, isLooping, isPlaying, src, volume } = audioInfo;
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
duration,
|
|
277
|
+
fileName,
|
|
278
|
+
isCurrentlyPlaying: queueSlotNumber === 0 && isPlaying,
|
|
279
|
+
isLooping,
|
|
280
|
+
src,
|
|
281
|
+
volume
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Gets the current queue length for a specific channel
|
|
287
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
288
|
+
* @returns Number of items in the queue, or 0 if channel doesn't exist
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* const queueSize = getQueueLength(0);
|
|
292
|
+
* console.log(`Channel 0 has ${queueSize} items in queue`);
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
export const getQueueLength = (channelNumber: number = 0): number => {
|
|
296
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
297
|
+
return channel ? channel.queue.length : 0;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Swaps the positions of two queue items
|
|
302
|
+
* Cannot swap with the currently playing item (index 0)
|
|
303
|
+
* @param slotA - Zero-based index of first item to swap (must be > 0)
|
|
304
|
+
* @param slotB - Zero-based index of second item to swap (must be > 0)
|
|
305
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
306
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* // Swap the second and third items in queue
|
|
310
|
+
* const result = await swapQueueItems(1, 2, 0);
|
|
311
|
+
* if (result.success) {
|
|
312
|
+
* console.log('Items swapped successfully');
|
|
313
|
+
* }
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
export const swapQueueItems = async (
|
|
317
|
+
slotA: number,
|
|
318
|
+
slotB: number,
|
|
319
|
+
channelNumber: number = 0
|
|
320
|
+
): Promise<QueueManipulationResult> => {
|
|
321
|
+
const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
|
|
322
|
+
|
|
323
|
+
if (!channel) {
|
|
324
|
+
return {
|
|
325
|
+
error: `Channel ${channelNumber} does not exist`,
|
|
326
|
+
success: false
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (slotA < 0 || slotA >= channel.queue.length) {
|
|
331
|
+
return {
|
|
332
|
+
error:
|
|
333
|
+
`Invalid slot A ${slotA}. Queue has ${channel.queue.length} items ` +
|
|
334
|
+
`(indices 0-${channel.queue.length - 1})`,
|
|
335
|
+
success: false
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (slotB < 0 || slotB >= channel.queue.length) {
|
|
340
|
+
return {
|
|
341
|
+
error:
|
|
342
|
+
`Invalid slot B ${slotB}. Queue has ${channel.queue.length} items ` +
|
|
343
|
+
`(indices 0-${channel.queue.length - 1})`,
|
|
344
|
+
success: false
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (slotA === 0 || slotB === 0) {
|
|
349
|
+
return {
|
|
350
|
+
error: 'Cannot swap with currently playing item (index 0)',
|
|
351
|
+
success: false
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (slotA === slotB) {
|
|
356
|
+
// No change needed, but return success
|
|
357
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
updatedQueue: updatedQueue ?? undefined
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Swap the audio elements
|
|
365
|
+
const temp: HTMLAudioElement = channel.queue[slotA];
|
|
366
|
+
channel.queue[slotA] = channel.queue[slotB];
|
|
367
|
+
channel.queue[slotB] = temp;
|
|
368
|
+
|
|
369
|
+
// Emit queue change event
|
|
370
|
+
emitQueueChange(channelNumber, audioChannels);
|
|
371
|
+
|
|
372
|
+
const updatedQueue: QueueSnapshot | null = createQueueSnapshot(channelNumber, audioChannels);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
success: true,
|
|
376
|
+
updatedQueue: updatedQueue ?? undefined
|
|
377
|
+
};
|
|
378
|
+
};
|