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
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Queue manipulation functions for the audio-channel-queue package
|
|
4
|
+
* Provides advanced queue management including item removal, reordering, and clearing
|
|
5
|
+
*/
|
|
6
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.swapQueueItems = exports.getQueueLength = exports.getQueueItemInfo = exports.clearQueueAfterCurrent = exports.reorderQueue = exports.removeQueuedItem = void 0;
|
|
17
|
+
const info_1 = require("./info");
|
|
18
|
+
const events_1 = require("./events");
|
|
19
|
+
const events_2 = require("./events");
|
|
20
|
+
const utils_1 = require("./utils");
|
|
21
|
+
/**
|
|
22
|
+
* Removes a specific item from the queue by its slot number (0-based index)
|
|
23
|
+
* Cannot remove the currently playing item (index 0) - use stopCurrentAudioInChannel instead
|
|
24
|
+
* @param queuedSlotNumber - Zero-based index of the item to remove (must be > 0)
|
|
25
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
26
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
27
|
+
* @throws Error if trying to remove currently playing item or invalid slot number
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // Remove the second item in queue (index 1)
|
|
31
|
+
* const result = await removeQueuedItem(1, 0);
|
|
32
|
+
* if (result.success) {
|
|
33
|
+
* console.log(`Removed item, queue now has ${result.updatedQueue.totalItems} items`);
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* // Remove the third item from channel 1
|
|
37
|
+
* await removeQueuedItem(2, 1);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
const removeQueuedItem = (queuedSlotNumber_1, ...args_1) => __awaiter(void 0, [queuedSlotNumber_1, ...args_1], void 0, function* (queuedSlotNumber, channelNumber = 0) {
|
|
41
|
+
const channel = info_1.audioChannels[channelNumber];
|
|
42
|
+
if (!channel) {
|
|
43
|
+
return {
|
|
44
|
+
error: `Channel ${channelNumber} does not exist`,
|
|
45
|
+
success: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (queuedSlotNumber < 0 || queuedSlotNumber >= channel.queue.length) {
|
|
49
|
+
return {
|
|
50
|
+
error: `Invalid slot number ${queuedSlotNumber}. Queue has ${channel.queue.length} items ` +
|
|
51
|
+
`(indices 0-${channel.queue.length - 1})`,
|
|
52
|
+
success: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (queuedSlotNumber === 0) {
|
|
56
|
+
return {
|
|
57
|
+
error: 'Cannot remove currently playing item (index 0). ' +
|
|
58
|
+
'Use stopCurrentAudioInChannel() instead',
|
|
59
|
+
success: false
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Remove the audio element from the queue
|
|
63
|
+
const removedAudio = channel.queue.splice(queuedSlotNumber, 1)[0];
|
|
64
|
+
// Clean up any progress tracking for the removed audio
|
|
65
|
+
(0, events_2.cleanupProgressTracking)(removedAudio, channelNumber, info_1.audioChannels);
|
|
66
|
+
// Emit queue change event
|
|
67
|
+
(0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
|
|
68
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
exports.removeQueuedItem = removeQueuedItem;
|
|
75
|
+
/**
|
|
76
|
+
* Reorders a queue item by moving it from one position to another
|
|
77
|
+
* Cannot reorder the currently playing item (index 0)
|
|
78
|
+
* @param currentQueuedSlotNumber - Current zero-based index of the item to move (must be > 0)
|
|
79
|
+
* @param newQueuedSlotNumber - New zero-based index where the item should be placed (must be > 0)
|
|
80
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
81
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
82
|
+
* @throws Error if trying to reorder currently playing item or invalid slot numbers
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // Move item from position 2 to position 1 (make it play next)
|
|
86
|
+
* const result = await reorderQueue(2, 1, 0);
|
|
87
|
+
* if (result.success) {
|
|
88
|
+
* console.log('Item moved successfully');
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* // Move item from position 1 to end of queue
|
|
92
|
+
* await reorderQueue(1, 4, 0); // Assuming queue has 5+ items
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
const reorderQueue = (currentQueuedSlotNumber_1, newQueuedSlotNumber_1, ...args_1) => __awaiter(void 0, [currentQueuedSlotNumber_1, newQueuedSlotNumber_1, ...args_1], void 0, function* (currentQueuedSlotNumber, newQueuedSlotNumber, channelNumber = 0) {
|
|
96
|
+
const channel = info_1.audioChannels[channelNumber];
|
|
97
|
+
if (!channel) {
|
|
98
|
+
return {
|
|
99
|
+
error: `Channel ${channelNumber} does not exist`,
|
|
100
|
+
success: false
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (currentQueuedSlotNumber < 0 || currentQueuedSlotNumber >= channel.queue.length) {
|
|
104
|
+
return {
|
|
105
|
+
error: `Invalid current slot number ${currentQueuedSlotNumber}. Queue has ` +
|
|
106
|
+
`${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
|
|
107
|
+
success: false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (newQueuedSlotNumber < 0 || newQueuedSlotNumber >= channel.queue.length) {
|
|
111
|
+
return {
|
|
112
|
+
error: `Invalid new slot number ${newQueuedSlotNumber}. Queue has ` +
|
|
113
|
+
`${channel.queue.length} items (indices 0-${channel.queue.length - 1})`,
|
|
114
|
+
success: false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (currentQueuedSlotNumber === 0) {
|
|
118
|
+
return {
|
|
119
|
+
error: 'Cannot reorder currently playing item (index 0). ' + 'Stop current audio first if needed',
|
|
120
|
+
success: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (newQueuedSlotNumber === 0) {
|
|
124
|
+
return {
|
|
125
|
+
error: 'Cannot move item to currently playing position (index 0). ' +
|
|
126
|
+
'Use queueAudioPriority() to add items to front of queue',
|
|
127
|
+
success: false
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (currentQueuedSlotNumber === newQueuedSlotNumber) {
|
|
131
|
+
// No change needed, but return success
|
|
132
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Remove the item from its current position
|
|
139
|
+
const audioToMove = channel.queue.splice(currentQueuedSlotNumber, 1)[0];
|
|
140
|
+
// Insert it at the new position
|
|
141
|
+
channel.queue.splice(newQueuedSlotNumber, 0, audioToMove);
|
|
142
|
+
// Emit queue change event
|
|
143
|
+
(0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
|
|
144
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
exports.reorderQueue = reorderQueue;
|
|
151
|
+
/**
|
|
152
|
+
* Clears all queued audio items after the currently playing item
|
|
153
|
+
* The current audio will continue playing but nothing will follow it
|
|
154
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
155
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* // Let current song finish but clear everything after it
|
|
159
|
+
* const result = await clearQueueAfterCurrent(0);
|
|
160
|
+
* if (result.success) {
|
|
161
|
+
* console.log(`Cleared queue, current audio will be the last to play`);
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
const clearQueueAfterCurrent = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
|
|
166
|
+
const channel = info_1.audioChannels[channelNumber];
|
|
167
|
+
if (!channel) {
|
|
168
|
+
// For empty/non-existent channels, we can consider this a successful no-op
|
|
169
|
+
// since there's nothing to clear anyway
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
updatedQueue: {
|
|
173
|
+
channelNumber,
|
|
174
|
+
currentIndex: -1,
|
|
175
|
+
isPaused: false,
|
|
176
|
+
items: [],
|
|
177
|
+
totalItems: 0,
|
|
178
|
+
volume: 1.0
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (channel.queue.length <= 1) {
|
|
183
|
+
// Nothing to clear - either empty queue or only current audio
|
|
184
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Clean up progress tracking for all items except the current one
|
|
191
|
+
for (let i = 1; i < channel.queue.length; i++) {
|
|
192
|
+
(0, events_2.cleanupProgressTracking)(channel.queue[i], channelNumber, info_1.audioChannels);
|
|
193
|
+
}
|
|
194
|
+
// Keep only the currently playing audio (index 0)
|
|
195
|
+
channel.queue = channel.queue.slice(0, 1);
|
|
196
|
+
// Emit queue change event
|
|
197
|
+
(0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
|
|
198
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
exports.clearQueueAfterCurrent = clearQueueAfterCurrent;
|
|
205
|
+
/**
|
|
206
|
+
* Gets information about a specific queue item by its slot number
|
|
207
|
+
* @param queueSlotNumber - Zero-based index of the queue item
|
|
208
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
209
|
+
* @returns QueueItem information or null if slot doesn't exist
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* const itemInfo = getQueueItemInfo(1, 0);
|
|
213
|
+
* if (itemInfo) {
|
|
214
|
+
* console.log(`Next to play: ${itemInfo.fileName}`);
|
|
215
|
+
* console.log(`Duration: ${itemInfo.duration}ms`);
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
const getQueueItemInfo = (queueSlotNumber, channelNumber = 0) => {
|
|
220
|
+
const channel = info_1.audioChannels[channelNumber];
|
|
221
|
+
if (!channel || queueSlotNumber < 0 || queueSlotNumber >= channel.queue.length) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const audio = channel.queue[queueSlotNumber];
|
|
225
|
+
const audioInfo = (0, utils_1.getAudioInfoFromElement)(audio, channelNumber, info_1.audioChannels);
|
|
226
|
+
if (!audioInfo) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const { duration, fileName, isLooping, isPlaying, src, volume } = audioInfo;
|
|
230
|
+
return {
|
|
231
|
+
duration,
|
|
232
|
+
fileName,
|
|
233
|
+
isCurrentlyPlaying: queueSlotNumber === 0 && isPlaying,
|
|
234
|
+
isLooping,
|
|
235
|
+
src,
|
|
236
|
+
volume
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
exports.getQueueItemInfo = getQueueItemInfo;
|
|
240
|
+
/**
|
|
241
|
+
* Gets the current queue length for a specific channel
|
|
242
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
243
|
+
* @returns Number of items in the queue, or 0 if channel doesn't exist
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* const queueSize = getQueueLength(0);
|
|
247
|
+
* console.log(`Channel 0 has ${queueSize} items in queue`);
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
const getQueueLength = (channelNumber = 0) => {
|
|
251
|
+
const channel = info_1.audioChannels[channelNumber];
|
|
252
|
+
return channel ? channel.queue.length : 0;
|
|
253
|
+
};
|
|
254
|
+
exports.getQueueLength = getQueueLength;
|
|
255
|
+
/**
|
|
256
|
+
* Swaps the positions of two queue items
|
|
257
|
+
* Cannot swap with the currently playing item (index 0)
|
|
258
|
+
* @param slotA - Zero-based index of first item to swap (must be > 0)
|
|
259
|
+
* @param slotB - Zero-based index of second item to swap (must be > 0)
|
|
260
|
+
* @param channelNumber - The channel number (defaults to 0)
|
|
261
|
+
* @returns Promise resolving to operation result with success status and updated queue
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* // Swap the second and third items in queue
|
|
265
|
+
* const result = await swapQueueItems(1, 2, 0);
|
|
266
|
+
* if (result.success) {
|
|
267
|
+
* console.log('Items swapped successfully');
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
const swapQueueItems = (slotA_1, slotB_1, ...args_1) => __awaiter(void 0, [slotA_1, slotB_1, ...args_1], void 0, function* (slotA, slotB, channelNumber = 0) {
|
|
272
|
+
const channel = info_1.audioChannels[channelNumber];
|
|
273
|
+
if (!channel) {
|
|
274
|
+
return {
|
|
275
|
+
error: `Channel ${channelNumber} does not exist`,
|
|
276
|
+
success: false
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (slotA < 0 || slotA >= channel.queue.length) {
|
|
280
|
+
return {
|
|
281
|
+
error: `Invalid slot A ${slotA}. Queue has ${channel.queue.length} items ` +
|
|
282
|
+
`(indices 0-${channel.queue.length - 1})`,
|
|
283
|
+
success: false
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (slotB < 0 || slotB >= channel.queue.length) {
|
|
287
|
+
return {
|
|
288
|
+
error: `Invalid slot B ${slotB}. Queue has ${channel.queue.length} items ` +
|
|
289
|
+
`(indices 0-${channel.queue.length - 1})`,
|
|
290
|
+
success: false
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (slotA === 0 || slotB === 0) {
|
|
294
|
+
return {
|
|
295
|
+
error: 'Cannot swap with currently playing item (index 0)',
|
|
296
|
+
success: false
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (slotA === slotB) {
|
|
300
|
+
// No change needed, but return success
|
|
301
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Swap the audio elements
|
|
308
|
+
const temp = channel.queue[slotA];
|
|
309
|
+
channel.queue[slotA] = channel.queue[slotB];
|
|
310
|
+
channel.queue[slotB] = temp;
|
|
311
|
+
// Emit queue change event
|
|
312
|
+
(0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
|
|
313
|
+
const updatedQueue = (0, utils_1.createQueueSnapshot)(channelNumber, info_1.audioChannels);
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
updatedQueue: updatedQueue !== null && updatedQueue !== void 0 ? updatedQueue : undefined
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
exports.swapQueueItems = swapQueueItems;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Type definitions for the audio-channel-queue package
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Maximum number of audio channels allowed to prevent memory exhaustion
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_CHANNELS: number;
|
|
4
8
|
/**
|
|
5
9
|
* Symbol used as a key for global (channel-wide) progress callbacks
|
|
6
10
|
* This avoids the need for `null as any` type assertions
|
|
@@ -34,18 +38,31 @@ export interface VolumeConfig {
|
|
|
34
38
|
transitionEasing?: EasingType;
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
|
-
*
|
|
41
|
+
* Configuration options for queuing audio
|
|
38
42
|
*/
|
|
39
43
|
export interface AudioQueueOptions {
|
|
40
|
-
/** Whether to add
|
|
44
|
+
/** Whether to add this audio to the front of the queue (after currently playing) */
|
|
41
45
|
addToFront?: boolean;
|
|
42
46
|
/** Whether the audio should loop when it finishes */
|
|
43
47
|
loop?: boolean;
|
|
44
|
-
/**
|
|
48
|
+
/** Maximum number of items allowed in the queue (defaults to unlimited) */
|
|
49
|
+
maxQueueSize?: number;
|
|
50
|
+
/** @deprecated Use addToFront instead. Legacy support for priority queuing */
|
|
45
51
|
priority?: boolean;
|
|
46
|
-
/** Volume level for this specific audio
|
|
52
|
+
/** Volume level for this specific audio (0-1) */
|
|
47
53
|
volume?: number;
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Global queue configuration options
|
|
57
|
+
*/
|
|
58
|
+
export interface QueueConfig {
|
|
59
|
+
/** Default maximum queue size across all channels (defaults to unlimited) */
|
|
60
|
+
defaultMaxQueueSize?: number;
|
|
61
|
+
/** Whether to drop oldest items when queue is full (defaults to false - reject new items) */
|
|
62
|
+
dropOldestWhenFull?: boolean;
|
|
63
|
+
/** Whether to show warnings when queue limits are reached (defaults to true) */
|
|
64
|
+
showQueueWarnings?: boolean;
|
|
65
|
+
}
|
|
49
66
|
/**
|
|
50
67
|
* Comprehensive audio information interface providing metadata about currently playing audio
|
|
51
68
|
*/
|
|
@@ -131,6 +148,17 @@ export interface QueueSnapshot {
|
|
|
131
148
|
/** Current volume level for the channel (0-1) */
|
|
132
149
|
volume: number;
|
|
133
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Information about a queue manipulation operation result
|
|
153
|
+
*/
|
|
154
|
+
export interface QueueManipulationResult {
|
|
155
|
+
/** Error message if operation failed */
|
|
156
|
+
error?: string;
|
|
157
|
+
/** Whether the operation was successful */
|
|
158
|
+
success: boolean;
|
|
159
|
+
/** The queue snapshot after the operation (if successful) */
|
|
160
|
+
updatedQueue?: QueueSnapshot;
|
|
161
|
+
}
|
|
134
162
|
/**
|
|
135
163
|
* Callback function type for audio progress updates
|
|
136
164
|
* @param info Current audio information
|
|
@@ -203,7 +231,7 @@ export interface ErrorRecoveryOptions {
|
|
|
203
231
|
*/
|
|
204
232
|
export type AudioErrorCallback = (errorInfo: AudioErrorInfo) => void;
|
|
205
233
|
/**
|
|
206
|
-
* Extended audio
|
|
234
|
+
* Extended audio channel with queue management and callback support
|
|
207
235
|
*/
|
|
208
236
|
export interface ExtendedAudioQueueChannel {
|
|
209
237
|
audioCompleteCallbacks: Set<AudioCompleteCallback>;
|
|
@@ -212,13 +240,16 @@ export interface ExtendedAudioQueueChannel {
|
|
|
212
240
|
audioResumeCallbacks: Set<AudioResumeCallback>;
|
|
213
241
|
audioStartCallbacks: Set<AudioStartCallback>;
|
|
214
242
|
fadeState?: ChannelFadeState;
|
|
215
|
-
isPaused
|
|
243
|
+
isPaused: boolean;
|
|
244
|
+
/** Active operation lock to prevent race conditions */
|
|
245
|
+
isLocked?: boolean;
|
|
246
|
+
/** Maximum allowed queue size for this channel */
|
|
247
|
+
maxQueueSize?: number;
|
|
216
248
|
progressCallbacks: Map<HTMLAudioElement | typeof GLOBAL_PROGRESS_KEY, Set<ProgressCallback>>;
|
|
217
249
|
queue: HTMLAudioElement[];
|
|
218
250
|
queueChangeCallbacks: Set<QueueChangeCallback>;
|
|
219
251
|
retryConfig?: RetryConfig;
|
|
220
|
-
volume
|
|
221
|
-
volumeConfig?: VolumeConfig;
|
|
252
|
+
volume: number;
|
|
222
253
|
}
|
|
223
254
|
/**
|
|
224
255
|
* Easing function types for volume transitions
|
|
@@ -237,6 +268,13 @@ export declare enum FadeType {
|
|
|
237
268
|
Gentle = "gentle",
|
|
238
269
|
Dramatic = "dramatic"
|
|
239
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Timer types for volume transitions to ensure proper cleanup
|
|
273
|
+
*/
|
|
274
|
+
export declare enum TimerType {
|
|
275
|
+
RequestAnimationFrame = "raf",
|
|
276
|
+
Timeout = "timeout"
|
|
277
|
+
}
|
|
240
278
|
/**
|
|
241
279
|
* Configuration for fade transitions
|
|
242
280
|
*/
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* @fileoverview Type definitions for the audio-channel-queue package
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.FadeType = exports.EasingType = exports.GLOBAL_PROGRESS_KEY = void 0;
|
|
6
|
+
exports.TimerType = exports.FadeType = exports.EasingType = exports.GLOBAL_PROGRESS_KEY = exports.MAX_CHANNELS = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of audio channels allowed to prevent memory exhaustion
|
|
9
|
+
*/
|
|
10
|
+
exports.MAX_CHANNELS = 64;
|
|
7
11
|
/**
|
|
8
12
|
* Symbol used as a key for global (channel-wide) progress callbacks
|
|
9
13
|
* This avoids the need for `null as any` type assertions
|
|
@@ -28,3 +32,11 @@ var FadeType;
|
|
|
28
32
|
FadeType["Gentle"] = "gentle";
|
|
29
33
|
FadeType["Dramatic"] = "dramatic";
|
|
30
34
|
})(FadeType || (exports.FadeType = FadeType = {}));
|
|
35
|
+
/**
|
|
36
|
+
* Timer types for volume transitions to ensure proper cleanup
|
|
37
|
+
*/
|
|
38
|
+
var TimerType;
|
|
39
|
+
(function (TimerType) {
|
|
40
|
+
TimerType["RequestAnimationFrame"] = "raf";
|
|
41
|
+
TimerType["Timeout"] = "timeout";
|
|
42
|
+
})(TimerType || (exports.TimerType = TimerType = {}));
|
package/dist/utils.d.ts
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
* @fileoverview Utility functions for the audio-channel-queue package
|
|
3
3
|
*/
|
|
4
4
|
import { AudioInfo, QueueSnapshot, ExtendedAudioQueueChannel } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Validates an audio URL for security and correctness
|
|
7
|
+
* @param url - The URL to validate
|
|
8
|
+
* @returns The validated URL
|
|
9
|
+
* @throws Error if the URL is invalid or potentially malicious
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* validateAudioUrl('https://example.com/audio.mp3'); // Valid
|
|
13
|
+
* validateAudioUrl('./sounds/local.wav'); // Valid relative path
|
|
14
|
+
* validateAudioUrl('javascript:alert("XSS")'); // Throws error
|
|
15
|
+
* validateAudioUrl('data:text/html,<script>alert("XSS")</script>'); // Throws error
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare const validateAudioUrl: (url: string) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Sanitizes a string for safe display in HTML contexts
|
|
21
|
+
* @param text - The text to sanitize
|
|
22
|
+
* @returns The sanitized text safe for display
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* sanitizeForDisplay('<script>alert("XSS")</script>'); // Returns: '<script>alert("XSS")</script>'
|
|
26
|
+
* sanitizeForDisplay('normal-file.mp3'); // Returns: 'normal-file.mp3'
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const sanitizeForDisplay: (text: string) => string;
|
|
5
30
|
/**
|
|
6
31
|
* Extracts the filename from a URL string
|
|
7
32
|
* @param url - The URL to extract the filename from
|
package/dist/utils.js
CHANGED
|
@@ -3,7 +3,92 @@
|
|
|
3
3
|
* @fileoverview Utility functions for the audio-channel-queue package
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.cleanWebpackFilename = exports.createQueueSnapshot = exports.getAudioInfoFromElement = exports.extractFileName = void 0;
|
|
6
|
+
exports.cleanWebpackFilename = exports.createQueueSnapshot = exports.getAudioInfoFromElement = exports.extractFileName = exports.sanitizeForDisplay = exports.validateAudioUrl = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Validates an audio URL for security and correctness
|
|
9
|
+
* @param url - The URL to validate
|
|
10
|
+
* @returns The validated URL
|
|
11
|
+
* @throws Error if the URL is invalid or potentially malicious
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* validateAudioUrl('https://example.com/audio.mp3'); // Valid
|
|
15
|
+
* validateAudioUrl('./sounds/local.wav'); // Valid relative path
|
|
16
|
+
* validateAudioUrl('javascript:alert("XSS")'); // Throws error
|
|
17
|
+
* validateAudioUrl('data:text/html,<script>alert("XSS")</script>'); // Throws error
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
const validateAudioUrl = (url) => {
|
|
21
|
+
if (!url || typeof url !== 'string') {
|
|
22
|
+
throw new Error('Audio URL must be a non-empty string');
|
|
23
|
+
}
|
|
24
|
+
// Trim whitespace
|
|
25
|
+
const trimmedUrl = url.trim();
|
|
26
|
+
// Check for dangerous protocols
|
|
27
|
+
const dangerousProtocols = [
|
|
28
|
+
'javascript:',
|
|
29
|
+
'data:',
|
|
30
|
+
'vbscript:',
|
|
31
|
+
'file:',
|
|
32
|
+
'about:',
|
|
33
|
+
'chrome:',
|
|
34
|
+
'chrome-extension:'
|
|
35
|
+
];
|
|
36
|
+
const lowerUrl = trimmedUrl.toLowerCase();
|
|
37
|
+
for (const protocol of dangerousProtocols) {
|
|
38
|
+
if (lowerUrl.startsWith(protocol)) {
|
|
39
|
+
throw new Error(`Invalid audio URL: dangerous protocol "${protocol}" is not allowed`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check for path traversal attempts
|
|
43
|
+
if (trimmedUrl.includes('../') || trimmedUrl.includes('..\\')) {
|
|
44
|
+
throw new Error('Invalid audio URL: path traversal attempts are not allowed');
|
|
45
|
+
}
|
|
46
|
+
// For relative URLs, ensure they don't start with dangerous characters
|
|
47
|
+
if (!trimmedUrl.startsWith('http://') && !trimmedUrl.startsWith('https://')) {
|
|
48
|
+
// Check for protocol-less URLs that might be interpreted as protocols
|
|
49
|
+
if (trimmedUrl.includes(':') && !trimmedUrl.startsWith('//')) {
|
|
50
|
+
const colonIndex = trimmedUrl.indexOf(':');
|
|
51
|
+
const beforeColon = trimmedUrl.substring(0, colonIndex);
|
|
52
|
+
// Allow only if it looks like a Windows drive letter (e.g., C:)
|
|
53
|
+
if (!/^[a-zA-Z]$/.test(beforeColon)) {
|
|
54
|
+
throw new Error('Invalid audio URL: suspicious protocol-like pattern detected');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Validate common audio file extensions (warning, not error)
|
|
59
|
+
const hasAudioExtension = /\.(mp3|wav|ogg|m4a|webm|aac|flac|opus|weba|mp4)$/i.test(trimmedUrl);
|
|
60
|
+
if (!hasAudioExtension && !trimmedUrl.includes('?')) {
|
|
61
|
+
// Log warning but don't throw - some valid URLs might not have extensions
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.warn(`Audio URL "${trimmedUrl}" does not have a recognized audio file extension`);
|
|
64
|
+
}
|
|
65
|
+
return trimmedUrl;
|
|
66
|
+
};
|
|
67
|
+
exports.validateAudioUrl = validateAudioUrl;
|
|
68
|
+
/**
|
|
69
|
+
* Sanitizes a string for safe display in HTML contexts
|
|
70
|
+
* @param text - The text to sanitize
|
|
71
|
+
* @returns The sanitized text safe for display
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* sanitizeForDisplay('<script>alert("XSS")</script>'); // Returns: '<script>alert("XSS")</script>'
|
|
75
|
+
* sanitizeForDisplay('normal-file.mp3'); // Returns: 'normal-file.mp3'
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
const sanitizeForDisplay = (text) => {
|
|
79
|
+
if (!text || typeof text !== 'string') {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
// Replace HTML special characters
|
|
83
|
+
return text
|
|
84
|
+
.replace(/&/g, '&')
|
|
85
|
+
.replace(/</g, '<')
|
|
86
|
+
.replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"')
|
|
88
|
+
.replace(/'/g, ''')
|
|
89
|
+
.replace(/\//g, '/');
|
|
90
|
+
};
|
|
91
|
+
exports.sanitizeForDisplay = sanitizeForDisplay;
|
|
7
92
|
/**
|
|
8
93
|
* Extracts the filename from a URL string
|
|
9
94
|
* @param url - The URL to extract the filename from
|
|
@@ -15,18 +100,21 @@ exports.cleanWebpackFilename = exports.createQueueSnapshot = exports.getAudioInf
|
|
|
15
100
|
* ```
|
|
16
101
|
*/
|
|
17
102
|
const extractFileName = (url) => {
|
|
103
|
+
if (!url || typeof url !== 'string') {
|
|
104
|
+
return (0, exports.sanitizeForDisplay)('unknown');
|
|
105
|
+
}
|
|
106
|
+
// Always use simple string manipulation for consistency
|
|
107
|
+
const segments = url.split('/');
|
|
108
|
+
const lastSegment = segments[segments.length - 1] || '';
|
|
109
|
+
// Remove query parameters and hash
|
|
110
|
+
const fileName = lastSegment.split('?')[0].split('#')[0];
|
|
111
|
+
// Decode URI components and sanitize
|
|
18
112
|
try {
|
|
19
|
-
|
|
20
|
-
const pathname = urlObj.pathname;
|
|
21
|
-
const segments = pathname.split('/');
|
|
22
|
-
const fileName = segments[segments.length - 1];
|
|
23
|
-
return fileName || 'unknown';
|
|
113
|
+
return (0, exports.sanitizeForDisplay)(decodeURIComponent(fileName || 'unknown'));
|
|
24
114
|
}
|
|
25
115
|
catch (_a) {
|
|
26
|
-
// If
|
|
27
|
-
|
|
28
|
-
const fileName = segments[segments.length - 1];
|
|
29
|
-
return fileName || 'unknown';
|
|
116
|
+
// If decoding fails, return the sanitized raw filename
|
|
117
|
+
return (0, exports.sanitizeForDisplay)(fileName || 'unknown');
|
|
30
118
|
}
|
|
31
119
|
};
|
|
32
120
|
exports.extractFileName = extractFileName;
|
package/dist/volume.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare const transitionVolume: (channelNumber: number, targetVolume: num
|
|
|
32
32
|
* @param volume - Volume level (0-1)
|
|
33
33
|
* @param transitionDuration - Optional transition duration in milliseconds
|
|
34
34
|
* @param easing - Optional easing function
|
|
35
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
35
36
|
* @example
|
|
36
37
|
* ```typescript
|
|
37
38
|
* setChannelVolume(0, 0.5); // Set channel 0 to 50%
|
|
@@ -76,6 +77,7 @@ export declare const setAllChannelsVolume: (volume: number) => Promise<void>;
|
|
|
76
77
|
* Configures volume ducking for channels. When the priority channel plays audio,
|
|
77
78
|
* all other channels will be automatically reduced to the ducking volume level
|
|
78
79
|
* @param config - Volume ducking configuration
|
|
80
|
+
* @throws Error if the priority channel number exceeds the maximum allowed channels
|
|
79
81
|
* @example
|
|
80
82
|
* ```typescript
|
|
81
83
|
* // When channel 1 plays, reduce all other channels to 20% volume
|
|
@@ -116,8 +118,19 @@ export declare const applyVolumeDucking: (activeChannelNumber: number) => Promis
|
|
|
116
118
|
*/
|
|
117
119
|
export declare const fadeVolume: (channelNumber: number, targetVolume: number, duration?: number, easing?: EasingType) => Promise<void>;
|
|
118
120
|
/**
|
|
119
|
-
* Restores normal volume levels when priority channel
|
|
121
|
+
* Restores normal volume levels when priority channel queue becomes empty
|
|
120
122
|
* @param stoppedChannelNumber - The channel that just stopped playing
|
|
121
123
|
* @internal
|
|
122
124
|
*/
|
|
123
125
|
export declare const restoreVolumeLevels: (stoppedChannelNumber: number) => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Cancels any active volume transition for a specific channel
|
|
128
|
+
* @param channelNumber - The channel number to cancel transitions for
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export declare const cancelVolumeTransition: (channelNumber: number) => void;
|
|
132
|
+
/**
|
|
133
|
+
* Cancels all active volume transitions across all channels
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
export declare const cancelAllVolumeTransitions: () => void;
|