audio-channel-queue 1.8.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 +333 -41
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +37 -18
- package/dist/events.js +21 -14
- package/dist/index.d.ts +9 -8
- package/dist/index.js +23 -3
- package/dist/info.d.ts +17 -6
- package/dist/info.js +89 -17
- package/dist/pause.d.ts +24 -12
- package/dist/pause.js +93 -41
- package/dist/queue-manipulation.d.ts +104 -0
- package/dist/queue-manipulation.js +319 -0
- package/dist/types.d.ts +58 -11
- package/dist/types.js +18 -1
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +102 -13
- package/dist/volume.d.ts +14 -1
- package/dist/volume.js +201 -60
- package/package.json +18 -3
- package/src/core.ts +437 -81
- package/src/errors.ts +516 -480
- package/src/events.ts +36 -27
- package/src/index.ts +68 -43
- package/src/info.ts +129 -30
- package/src/pause.ts +169 -88
- package/src/queue-manipulation.ts +378 -0
- package/src/types.ts +63 -11
- package/src/utils.ts +117 -16
- package/src/volume.ts +250 -81
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;
|
|
@@ -56,7 +144,7 @@ const getAudioInfoFromElement = (audio, channelNumber, audioChannels) => {
|
|
|
56
144
|
const isPlaying = !audio.paused && !audio.ended && audio.readyState > 2;
|
|
57
145
|
// Calculate remainingInQueue if channel context is provided
|
|
58
146
|
let remainingInQueue = 0;
|
|
59
|
-
if (channelNumber !== undefined && audioChannels
|
|
147
|
+
if (channelNumber !== undefined && (audioChannels === null || audioChannels === void 0 ? void 0 : audioChannels[channelNumber])) {
|
|
60
148
|
const channel = audioChannels[channelNumber];
|
|
61
149
|
remainingInQueue = Math.max(0, channel.queue.length - 1); // Exclude current playing audio
|
|
62
150
|
}
|
|
@@ -86,6 +174,7 @@ exports.getAudioInfoFromElement = getAudioInfoFromElement;
|
|
|
86
174
|
* ```
|
|
87
175
|
*/
|
|
88
176
|
const createQueueSnapshot = (channelNumber, audioChannels) => {
|
|
177
|
+
var _a, _b;
|
|
89
178
|
const channel = audioChannels[channelNumber];
|
|
90
179
|
if (!channel)
|
|
91
180
|
return null;
|
|
@@ -100,10 +189,10 @@ const createQueueSnapshot = (channelNumber, audioChannels) => {
|
|
|
100
189
|
return {
|
|
101
190
|
channelNumber,
|
|
102
191
|
currentIndex: 0, // Current playing is always index 0 in our queue structure
|
|
103
|
-
isPaused: channel.isPaused
|
|
192
|
+
isPaused: (_a = channel.isPaused) !== null && _a !== void 0 ? _a : false,
|
|
104
193
|
items,
|
|
105
194
|
totalItems: channel.queue.length,
|
|
106
|
-
volume: channel.volume
|
|
195
|
+
volume: (_b = channel.volume) !== null && _b !== void 0 ? _b : 1.0
|
|
107
196
|
};
|
|
108
197
|
};
|
|
109
198
|
exports.createQueueSnapshot = createQueueSnapshot;
|
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;
|
package/dist/volume.js
CHANGED
|
@@ -12,18 +12,37 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
12
12
|
});
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.restoreVolumeLevels = exports.fadeVolume = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = exports.getFadeConfig = void 0;
|
|
15
|
+
exports.cancelAllVolumeTransitions = exports.cancelVolumeTransition = exports.restoreVolumeLevels = exports.fadeVolume = exports.applyVolumeDucking = exports.clearVolumeDucking = exports.setVolumeDucking = exports.setAllChannelsVolume = exports.getAllChannelsVolume = exports.getChannelVolume = exports.setChannelVolume = exports.transitionVolume = exports.getFadeConfig = void 0;
|
|
16
16
|
const types_1 = require("./types");
|
|
17
17
|
const info_1 = require("./info");
|
|
18
18
|
// Store active volume transitions to handle interruptions
|
|
19
19
|
const activeTransitions = new Map();
|
|
20
|
+
// Track which timer type was used for each channel
|
|
21
|
+
const timerTypes = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Global volume ducking configuration
|
|
24
|
+
* Stores the volume ducking settings that apply to all channels
|
|
25
|
+
*/
|
|
26
|
+
let globalVolumeConfig = null;
|
|
20
27
|
/**
|
|
21
28
|
* Predefined fade configurations for different transition types
|
|
22
29
|
*/
|
|
23
|
-
const
|
|
24
|
-
[types_1.FadeType.Dramatic]: {
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const fadeConfigs = {
|
|
31
|
+
[types_1.FadeType.Dramatic]: {
|
|
32
|
+
duration: 800,
|
|
33
|
+
pauseCurve: types_1.EasingType.EaseIn,
|
|
34
|
+
resumeCurve: types_1.EasingType.EaseOut
|
|
35
|
+
},
|
|
36
|
+
[types_1.FadeType.Gentle]: {
|
|
37
|
+
duration: 800,
|
|
38
|
+
pauseCurve: types_1.EasingType.EaseOut,
|
|
39
|
+
resumeCurve: types_1.EasingType.EaseIn
|
|
40
|
+
},
|
|
41
|
+
[types_1.FadeType.Linear]: {
|
|
42
|
+
duration: 800,
|
|
43
|
+
pauseCurve: types_1.EasingType.Linear,
|
|
44
|
+
resumeCurve: types_1.EasingType.Linear
|
|
45
|
+
}
|
|
27
46
|
};
|
|
28
47
|
/**
|
|
29
48
|
* Gets the fade configuration for a specific fade type
|
|
@@ -36,7 +55,7 @@ const FADE_CONFIGS = {
|
|
|
36
55
|
* ```
|
|
37
56
|
*/
|
|
38
57
|
const getFadeConfig = (fadeType) => {
|
|
39
|
-
return Object.assign({},
|
|
58
|
+
return Object.assign({}, fadeConfigs[fadeType]);
|
|
40
59
|
};
|
|
41
60
|
exports.getFadeConfig = getFadeConfig;
|
|
42
61
|
/**
|
|
@@ -46,7 +65,7 @@ const easingFunctions = {
|
|
|
46
65
|
[types_1.EasingType.Linear]: (t) => t,
|
|
47
66
|
[types_1.EasingType.EaseIn]: (t) => t * t,
|
|
48
67
|
[types_1.EasingType.EaseOut]: (t) => t * (2 - t),
|
|
49
|
-
[types_1.EasingType.EaseInOut]: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
68
|
+
[types_1.EasingType.EaseInOut]: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
|
|
50
69
|
};
|
|
51
70
|
/**
|
|
52
71
|
* Smoothly transitions volume for a specific channel over time
|
|
@@ -69,16 +88,28 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
|
|
|
69
88
|
const volumeDelta = targetVolume - startVolume;
|
|
70
89
|
// Cancel any existing transition for this channel
|
|
71
90
|
if (activeTransitions.has(channelNumber)) {
|
|
72
|
-
|
|
91
|
+
const transitionId = activeTransitions.get(channelNumber);
|
|
92
|
+
const timerType = timerTypes.get(channelNumber);
|
|
93
|
+
if (transitionId) {
|
|
94
|
+
// Cancel based on the timer type that was actually used
|
|
95
|
+
if (timerType === types_1.TimerType.RequestAnimationFrame &&
|
|
96
|
+
typeof cancelAnimationFrame !== 'undefined') {
|
|
97
|
+
cancelAnimationFrame(transitionId);
|
|
98
|
+
}
|
|
99
|
+
else if (timerType === types_1.TimerType.Timeout) {
|
|
100
|
+
clearTimeout(transitionId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
73
103
|
activeTransitions.delete(channelNumber);
|
|
104
|
+
timerTypes.delete(channelNumber);
|
|
74
105
|
}
|
|
75
106
|
// If no change needed, resolve immediately
|
|
76
107
|
if (Math.abs(volumeDelta) < 0.001) {
|
|
77
108
|
channel.volume = targetVolume;
|
|
78
109
|
return Promise.resolve();
|
|
79
110
|
}
|
|
80
|
-
// Handle zero duration - instant change
|
|
81
|
-
if (duration
|
|
111
|
+
// Handle zero or negative duration - instant change
|
|
112
|
+
if (duration <= 0) {
|
|
82
113
|
channel.volume = targetVolume;
|
|
83
114
|
if (channel.queue.length > 0) {
|
|
84
115
|
channel.queue[0].volume = targetVolume;
|
|
@@ -92,7 +123,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
|
|
|
92
123
|
const elapsed = performance.now() - startTime;
|
|
93
124
|
const progress = Math.min(elapsed / duration, 1);
|
|
94
125
|
const easedProgress = easingFn(progress);
|
|
95
|
-
const currentVolume = startVolume +
|
|
126
|
+
const currentVolume = startVolume + volumeDelta * easedProgress;
|
|
96
127
|
const clampedVolume = Math.max(0, Math.min(1, currentVolume));
|
|
97
128
|
// Apply volume to both channel config and current audio
|
|
98
129
|
channel.volume = clampedVolume;
|
|
@@ -102,6 +133,7 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
|
|
|
102
133
|
if (progress >= 1) {
|
|
103
134
|
// Transition complete
|
|
104
135
|
activeTransitions.delete(channelNumber);
|
|
136
|
+
timerTypes.delete(channelNumber);
|
|
105
137
|
resolve();
|
|
106
138
|
}
|
|
107
139
|
else {
|
|
@@ -109,11 +141,13 @@ const transitionVolume = (channelNumber_1, targetVolume_1, ...args_1) => __await
|
|
|
109
141
|
if (typeof requestAnimationFrame !== 'undefined') {
|
|
110
142
|
const rafId = requestAnimationFrame(updateVolume);
|
|
111
143
|
activeTransitions.set(channelNumber, rafId);
|
|
144
|
+
timerTypes.set(channelNumber, types_1.TimerType.RequestAnimationFrame);
|
|
112
145
|
}
|
|
113
146
|
else {
|
|
114
147
|
// In test environment, use shorter intervals
|
|
115
148
|
const timeoutId = setTimeout(updateVolume, 1);
|
|
116
149
|
activeTransitions.set(channelNumber, timeoutId);
|
|
150
|
+
timerTypes.set(channelNumber, types_1.TimerType.Timeout);
|
|
117
151
|
}
|
|
118
152
|
}
|
|
119
153
|
};
|
|
@@ -127,6 +161,7 @@ exports.transitionVolume = transitionVolume;
|
|
|
127
161
|
* @param volume - Volume level (0-1)
|
|
128
162
|
* @param transitionDuration - Optional transition duration in milliseconds
|
|
129
163
|
* @param easing - Optional easing function
|
|
164
|
+
* @throws Error if the channel number exceeds the maximum allowed channels
|
|
130
165
|
* @example
|
|
131
166
|
* ```typescript
|
|
132
167
|
* setChannelVolume(0, 0.5); // Set channel 0 to 50%
|
|
@@ -135,6 +170,13 @@ exports.transitionVolume = transitionVolume;
|
|
|
135
170
|
*/
|
|
136
171
|
const setChannelVolume = (channelNumber, volume, transitionDuration, easing) => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
172
|
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
173
|
+
// Check channel number limits
|
|
174
|
+
if (channelNumber < 0) {
|
|
175
|
+
throw new Error('Channel number must be non-negative');
|
|
176
|
+
}
|
|
177
|
+
if (channelNumber >= types_1.MAX_CHANNELS) {
|
|
178
|
+
throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
|
|
179
|
+
}
|
|
138
180
|
if (!info_1.audioChannels[channelNumber]) {
|
|
139
181
|
info_1.audioChannels[channelNumber] = {
|
|
140
182
|
audioCompleteCallbacks: new Set(),
|
|
@@ -177,8 +219,9 @@ exports.setChannelVolume = setChannelVolume;
|
|
|
177
219
|
* ```
|
|
178
220
|
*/
|
|
179
221
|
const getChannelVolume = (channelNumber = 0) => {
|
|
222
|
+
var _a;
|
|
180
223
|
const channel = info_1.audioChannels[channelNumber];
|
|
181
|
-
return (channel === null || channel === void 0 ? void 0 : channel.volume)
|
|
224
|
+
return (_a = channel === null || channel === void 0 ? void 0 : channel.volume) !== null && _a !== void 0 ? _a : 1.0;
|
|
182
225
|
};
|
|
183
226
|
exports.getChannelVolume = getChannelVolume;
|
|
184
227
|
/**
|
|
@@ -193,7 +236,7 @@ exports.getChannelVolume = getChannelVolume;
|
|
|
193
236
|
* ```
|
|
194
237
|
*/
|
|
195
238
|
const getAllChannelsVolume = () => {
|
|
196
|
-
return info_1.audioChannels.map((channel) => (channel === null || channel === void 0 ? void 0 : channel.volume)
|
|
239
|
+
return info_1.audioChannels.map((channel) => { var _a; return (_a = channel === null || channel === void 0 ? void 0 : channel.volume) !== null && _a !== void 0 ? _a : 1.0; });
|
|
197
240
|
};
|
|
198
241
|
exports.getAllChannelsVolume = getAllChannelsVolume;
|
|
199
242
|
/**
|
|
@@ -216,6 +259,7 @@ exports.setAllChannelsVolume = setAllChannelsVolume;
|
|
|
216
259
|
* Configures volume ducking for channels. When the priority channel plays audio,
|
|
217
260
|
* all other channels will be automatically reduced to the ducking volume level
|
|
218
261
|
* @param config - Volume ducking configuration
|
|
262
|
+
* @throws Error if the priority channel number exceeds the maximum allowed channels
|
|
219
263
|
* @example
|
|
220
264
|
* ```typescript
|
|
221
265
|
* // When channel 1 plays, reduce all other channels to 20% volume
|
|
@@ -227,8 +271,18 @@ exports.setAllChannelsVolume = setAllChannelsVolume;
|
|
|
227
271
|
* ```
|
|
228
272
|
*/
|
|
229
273
|
const setVolumeDucking = (config) => {
|
|
230
|
-
|
|
231
|
-
|
|
274
|
+
const { priorityChannel } = config;
|
|
275
|
+
// Check priority channel limits
|
|
276
|
+
if (priorityChannel < 0) {
|
|
277
|
+
throw new Error('Priority channel number must be non-negative');
|
|
278
|
+
}
|
|
279
|
+
if (priorityChannel >= types_1.MAX_CHANNELS) {
|
|
280
|
+
throw new Error(`Priority channel ${priorityChannel} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
|
|
281
|
+
}
|
|
282
|
+
// Store the configuration globally
|
|
283
|
+
globalVolumeConfig = config;
|
|
284
|
+
// Ensure we have enough channels for the priority channel
|
|
285
|
+
while (info_1.audioChannels.length <= priorityChannel) {
|
|
232
286
|
info_1.audioChannels.push({
|
|
233
287
|
audioCompleteCallbacks: new Set(),
|
|
234
288
|
audioErrorCallbacks: new Set(),
|
|
@@ -242,24 +296,6 @@ const setVolumeDucking = (config) => {
|
|
|
242
296
|
volume: 1.0
|
|
243
297
|
});
|
|
244
298
|
}
|
|
245
|
-
// Apply the config to all existing channels
|
|
246
|
-
info_1.audioChannels.forEach((channel, index) => {
|
|
247
|
-
if (!info_1.audioChannels[index]) {
|
|
248
|
-
info_1.audioChannels[index] = {
|
|
249
|
-
audioCompleteCallbacks: new Set(),
|
|
250
|
-
audioErrorCallbacks: new Set(),
|
|
251
|
-
audioPauseCallbacks: new Set(),
|
|
252
|
-
audioResumeCallbacks: new Set(),
|
|
253
|
-
audioStartCallbacks: new Set(),
|
|
254
|
-
isPaused: false,
|
|
255
|
-
progressCallbacks: new Map(),
|
|
256
|
-
queue: [],
|
|
257
|
-
queueChangeCallbacks: new Set(),
|
|
258
|
-
volume: 1.0
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
info_1.audioChannels[index].volumeConfig = config;
|
|
262
|
-
});
|
|
263
299
|
};
|
|
264
300
|
exports.setVolumeDucking = setVolumeDucking;
|
|
265
301
|
/**
|
|
@@ -270,11 +306,7 @@ exports.setVolumeDucking = setVolumeDucking;
|
|
|
270
306
|
* ```
|
|
271
307
|
*/
|
|
272
308
|
const clearVolumeDucking = () => {
|
|
273
|
-
|
|
274
|
-
if (channel) {
|
|
275
|
-
delete channel.volumeConfig;
|
|
276
|
-
}
|
|
277
|
-
});
|
|
309
|
+
globalVolumeConfig = null;
|
|
278
310
|
};
|
|
279
311
|
exports.clearVolumeDucking = clearVolumeDucking;
|
|
280
312
|
/**
|
|
@@ -283,21 +315,31 @@ exports.clearVolumeDucking = clearVolumeDucking;
|
|
|
283
315
|
* @internal
|
|
284
316
|
*/
|
|
285
317
|
const applyVolumeDucking = (activeChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
|
|
318
|
+
var _a, _b;
|
|
319
|
+
// Check if ducking is configured and this channel is the priority channel
|
|
320
|
+
if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== activeChannelNumber) {
|
|
321
|
+
return; // No ducking configured for this channel
|
|
322
|
+
}
|
|
323
|
+
const config = globalVolumeConfig;
|
|
286
324
|
const transitionPromises = [];
|
|
325
|
+
const duration = (_a = config.duckTransitionDuration) !== null && _a !== void 0 ? _a : 250;
|
|
326
|
+
const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
|
|
327
|
+
// Duck all channels except the priority channel
|
|
287
328
|
info_1.audioChannels.forEach((channel, channelNumber) => {
|
|
288
|
-
if (channel
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
329
|
+
if (!channel || channel.queue.length === 0) {
|
|
330
|
+
return; // Skip channels without audio
|
|
331
|
+
}
|
|
332
|
+
if (channelNumber === activeChannelNumber) {
|
|
333
|
+
// This is the priority channel - set to priority volume
|
|
334
|
+
// Only change audio volume, preserve channel.volume as desired volume
|
|
335
|
+
const currentAudio = channel.queue[0];
|
|
336
|
+
transitionPromises.push(transitionAudioVolume(currentAudio, config.priorityVolume, duration, easing));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// This is a background channel - duck it
|
|
340
|
+
// Only change audio volume, preserve channel.volume as desired volume
|
|
341
|
+
const currentAudio = channel.queue[0];
|
|
342
|
+
transitionPromises.push(transitionAudioVolume(currentAudio, config.duckingVolume, duration, easing));
|
|
301
343
|
}
|
|
302
344
|
});
|
|
303
345
|
// Wait for all transitions to complete
|
|
@@ -322,24 +364,123 @@ const fadeVolume = (channelNumber_1, targetVolume_1, ...args_1) => __awaiter(voi
|
|
|
322
364
|
});
|
|
323
365
|
exports.fadeVolume = fadeVolume;
|
|
324
366
|
/**
|
|
325
|
-
* Restores normal volume levels when priority channel
|
|
367
|
+
* Restores normal volume levels when priority channel queue becomes empty
|
|
326
368
|
* @param stoppedChannelNumber - The channel that just stopped playing
|
|
327
369
|
* @internal
|
|
328
370
|
*/
|
|
329
371
|
const restoreVolumeLevels = (stoppedChannelNumber) => __awaiter(void 0, void 0, void 0, function* () {
|
|
372
|
+
// Check if ducking is configured and this channel is the priority channel
|
|
373
|
+
if (!globalVolumeConfig || globalVolumeConfig.priorityChannel !== stoppedChannelNumber) {
|
|
374
|
+
return; // No ducking configured for this channel
|
|
375
|
+
}
|
|
376
|
+
// Check if the priority channel queue is now empty
|
|
377
|
+
const priorityChannel = info_1.audioChannels[stoppedChannelNumber];
|
|
378
|
+
if (priorityChannel && priorityChannel.queue.length > 0) {
|
|
379
|
+
return; // Priority channel still has audio queued, don't restore yet
|
|
380
|
+
}
|
|
381
|
+
const config = globalVolumeConfig;
|
|
330
382
|
const transitionPromises = [];
|
|
383
|
+
// Restore volume for all channels EXCEPT the priority channel
|
|
331
384
|
info_1.audioChannels.forEach((channel, channelNumber) => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const easing = config.transitionEasing || types_1.EasingType.EaseOut;
|
|
337
|
-
// Priority channel stopped, restore normal volumes
|
|
338
|
-
transitionPromises.push((0, exports.transitionVolume)(channelNumber, channel.volume || 1.0, duration, easing));
|
|
339
|
-
}
|
|
385
|
+
var _a, _b, _c;
|
|
386
|
+
// Skip the priority channel itself and channels without audio
|
|
387
|
+
if (channelNumber === stoppedChannelNumber || !channel || channel.queue.length === 0) {
|
|
388
|
+
return;
|
|
340
389
|
}
|
|
390
|
+
// Restore this channel to its desired volume
|
|
391
|
+
const duration = (_a = config.restoreTransitionDuration) !== null && _a !== void 0 ? _a : 500;
|
|
392
|
+
const easing = (_b = config.transitionEasing) !== null && _b !== void 0 ? _b : types_1.EasingType.EaseOut;
|
|
393
|
+
const targetVolume = (_c = channel.volume) !== null && _c !== void 0 ? _c : 1.0;
|
|
394
|
+
// Only transition the audio element volume, keep channel.volume as the desired volume
|
|
395
|
+
const currentAudio = channel.queue[0];
|
|
396
|
+
transitionPromises.push(transitionAudioVolume(currentAudio, targetVolume, duration, easing));
|
|
341
397
|
});
|
|
342
398
|
// Wait for all transitions to complete
|
|
343
399
|
yield Promise.all(transitionPromises);
|
|
344
400
|
});
|
|
345
401
|
exports.restoreVolumeLevels = restoreVolumeLevels;
|
|
402
|
+
/**
|
|
403
|
+
* Transitions only the audio element volume without affecting channel.volume
|
|
404
|
+
* This is used for ducking/restoration where channel.volume represents desired volume
|
|
405
|
+
* @param audio - The audio element to transition
|
|
406
|
+
* @param targetVolume - Target volume level (0-1)
|
|
407
|
+
* @param duration - Transition duration in milliseconds
|
|
408
|
+
* @param easing - Easing function type
|
|
409
|
+
* @returns Promise that resolves when transition completes
|
|
410
|
+
* @internal
|
|
411
|
+
*/
|
|
412
|
+
const transitionAudioVolume = (audio_1, targetVolume_1, ...args_1) => __awaiter(void 0, [audio_1, targetVolume_1, ...args_1], void 0, function* (audio, targetVolume, duration = 250, easing = types_1.EasingType.EaseOut) {
|
|
413
|
+
const startVolume = audio.volume;
|
|
414
|
+
const volumeDelta = targetVolume - startVolume;
|
|
415
|
+
// If no change needed, resolve immediately
|
|
416
|
+
if (Math.abs(volumeDelta) < 0.001) {
|
|
417
|
+
return Promise.resolve();
|
|
418
|
+
}
|
|
419
|
+
// Handle zero or negative duration - instant change
|
|
420
|
+
if (duration <= 0) {
|
|
421
|
+
audio.volume = Math.max(0, Math.min(1, targetVolume));
|
|
422
|
+
return Promise.resolve();
|
|
423
|
+
}
|
|
424
|
+
const startTime = performance.now();
|
|
425
|
+
const easingFn = easingFunctions[easing];
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
const updateVolume = () => {
|
|
428
|
+
const elapsed = performance.now() - startTime;
|
|
429
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
430
|
+
const easedProgress = easingFn(progress);
|
|
431
|
+
const currentVolume = startVolume + volumeDelta * easedProgress;
|
|
432
|
+
const clampedVolume = Math.max(0, Math.min(1, currentVolume));
|
|
433
|
+
// Only apply volume to audio element, not channel.volume
|
|
434
|
+
audio.volume = clampedVolume;
|
|
435
|
+
if (progress >= 1) {
|
|
436
|
+
resolve();
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Use requestAnimationFrame in browser, setTimeout in tests
|
|
440
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
441
|
+
requestAnimationFrame(updateVolume);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
setTimeout(updateVolume, 1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
updateVolume();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
/**
|
|
452
|
+
* Cancels any active volume transition for a specific channel
|
|
453
|
+
* @param channelNumber - The channel number to cancel transitions for
|
|
454
|
+
* @internal
|
|
455
|
+
*/
|
|
456
|
+
const cancelVolumeTransition = (channelNumber) => {
|
|
457
|
+
if (activeTransitions.has(channelNumber)) {
|
|
458
|
+
const transitionId = activeTransitions.get(channelNumber);
|
|
459
|
+
const timerType = timerTypes.get(channelNumber);
|
|
460
|
+
if (transitionId) {
|
|
461
|
+
// Cancel based on the timer type that was actually used
|
|
462
|
+
if (timerType === types_1.TimerType.RequestAnimationFrame &&
|
|
463
|
+
typeof cancelAnimationFrame !== 'undefined') {
|
|
464
|
+
cancelAnimationFrame(transitionId);
|
|
465
|
+
}
|
|
466
|
+
else if (timerType === types_1.TimerType.Timeout) {
|
|
467
|
+
clearTimeout(transitionId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
activeTransitions.delete(channelNumber);
|
|
471
|
+
timerTypes.delete(channelNumber);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
exports.cancelVolumeTransition = cancelVolumeTransition;
|
|
475
|
+
/**
|
|
476
|
+
* Cancels all active volume transitions across all channels
|
|
477
|
+
* @internal
|
|
478
|
+
*/
|
|
479
|
+
const cancelAllVolumeTransitions = () => {
|
|
480
|
+
// Get all active channel numbers to avoid modifying Map while iterating
|
|
481
|
+
const activeChannels = Array.from(activeTransitions.keys());
|
|
482
|
+
activeChannels.forEach((channelNumber) => {
|
|
483
|
+
(0, exports.cancelVolumeTransition)(channelNumber);
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
exports.cancelAllVolumeTransitions = cancelAllVolumeTransitions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "audio-channel-queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Allows you to queue audio files to different playback channels.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,20 @@
|
|
|
13
13
|
"prepare": "npm run build",
|
|
14
14
|
"test": "jest",
|
|
15
15
|
"test:watch": "jest --watch",
|
|
16
|
-
"test:coverage": "jest --coverage"
|
|
16
|
+
"test:coverage": "jest --coverage",
|
|
17
|
+
"lint": "npx eslint src/**/*.ts __tests__/**/*.ts",
|
|
18
|
+
"lint:fix": "npx eslint src/**/*.ts __tests__/**/*.ts --fix",
|
|
19
|
+
"format": "npx prettier --write src/**/*.ts __tests__/**/*.ts",
|
|
20
|
+
"format:check": "npx prettier --check src/**/*.ts __tests__/**/*.ts",
|
|
21
|
+
"clean": "rimraf dist",
|
|
22
|
+
"prebuild": "npm run clean",
|
|
23
|
+
"prepack": "npm run validate && npm run build",
|
|
24
|
+
"validate": "npm run format:check && npm run lint && npm run test",
|
|
25
|
+
"publish:patch": "npm version patch && npm run publish:release",
|
|
26
|
+
"publish:minor": "npm version minor && npm run publish:release",
|
|
27
|
+
"publish:major": "npm version major && npm run publish:release",
|
|
28
|
+
"publish:release": "npm run validate && npm run build && npm publish",
|
|
29
|
+
"publish:dry": "npm run validate && npm run build && npm publish --dry-run"
|
|
17
30
|
},
|
|
18
31
|
"repository": {
|
|
19
32
|
"type": "git",
|
|
@@ -38,7 +51,9 @@
|
|
|
38
51
|
"@types/jest": "^29.5.13",
|
|
39
52
|
"jest": "^29.7.0",
|
|
40
53
|
"jest-environment-jsdom": "^29.7.0",
|
|
54
|
+
"rimraf": "^6.0.1",
|
|
41
55
|
"ts-jest": "^29.2.5",
|
|
42
|
-
"typescript": "^5.6.2"
|
|
56
|
+
"typescript": "^5.6.2",
|
|
57
|
+
"typescript-eslint": "^8.34.1"
|
|
43
58
|
}
|
|
44
59
|
}
|