audio-channel-queue 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pause.ts CHANGED
@@ -2,10 +2,309 @@
2
2
  * @fileoverview Pause and resume management functions for the audio-channel-queue package
3
3
  */
4
4
 
5
- import { ExtendedAudioQueueChannel, AudioInfo } from './types';
5
+ import {
6
+ ExtendedAudioQueueChannel,
7
+ AudioInfo,
8
+ FadeType,
9
+ FadeConfig,
10
+ ChannelFadeState
11
+ } from './types';
6
12
  import { audioChannels } from './info';
7
13
  import { getAudioInfoFromElement } from './utils';
8
14
  import { emitAudioPause, emitAudioResume } from './events';
15
+ import { transitionVolume, getFadeConfig } from './volume';
16
+
17
+
18
+
19
+ /**
20
+ * Gets the current volume for a channel, accounting for synchronous state
21
+ * @param channelNumber - The channel number
22
+ * @returns Current volume level (0-1)
23
+ */
24
+ const getChannelVolumeSync = (channelNumber: number): number => {
25
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
26
+ return channel?.volume ?? 1.0;
27
+ };
28
+
29
+ /**
30
+ * Sets the channel volume synchronously in internal state
31
+ * @param channelNumber - The channel number
32
+ * @param volume - Volume level (0-1)
33
+ */
34
+ const setChannelVolumeSync = (channelNumber: number, volume: number): void => {
35
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
36
+ if (channel) {
37
+ channel.volume = volume;
38
+ if (channel.queue.length > 0) {
39
+ channel.queue[0].volume = volume;
40
+ }
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Pauses the currently playing audio in a specific channel with smooth volume fade
46
+ * @param fadeType - Type of fade transition to apply
47
+ * @param channelNumber - The channel number to pause (defaults to 0)
48
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
49
+ * @returns Promise that resolves when the pause and fade are complete
50
+ * @example
51
+ * ```typescript
52
+ * await pauseWithFade(FadeType.Gentle, 0); // Pause with gentle fade out over 800ms
53
+ * await pauseWithFade(FadeType.Dramatic, 1, 1500); // Pause with dramatic fade out over 1.5s
54
+ * await pauseWithFade(FadeType.Linear, 2, 500); // Linear pause with custom 500ms fade
55
+ * ```
56
+ */
57
+ export const pauseWithFade = async (
58
+ fadeType: FadeType = FadeType.Gentle,
59
+ channelNumber: number = 0,
60
+ duration?: number
61
+ ): Promise<void> => {
62
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
63
+
64
+ if (!channel || channel.queue.length === 0) return;
65
+
66
+ const currentAudio: HTMLAudioElement = channel.queue[0];
67
+
68
+ // Don't pause if already paused or ended
69
+ if (currentAudio.paused || currentAudio.ended) return;
70
+
71
+ const config: FadeConfig = getFadeConfig(fadeType);
72
+ const effectiveDuration: number = duration ?? config.duration;
73
+
74
+ // Race condition fix: Use existing fadeState originalVolume if already transitioning,
75
+ // otherwise capture current volume
76
+ let originalVolume: number;
77
+ if (channel.fadeState?.isTransitioning) {
78
+ // We're already in any kind of transition (pause or resume), preserve original volume
79
+ originalVolume = channel.fadeState.originalVolume;
80
+ } else {
81
+ // First fade or no transition in progress, capture current volume
82
+ // But ensure we don't capture a volume of 0 during a transition
83
+ const currentVolume = getChannelVolumeSync(channelNumber);
84
+ originalVolume = currentVolume > 0 ? currentVolume : channel.fadeState?.originalVolume ?? 1.0;
85
+ }
86
+
87
+ // Store fade state for resumeWithFade to use (including custom duration)
88
+ channel.fadeState = {
89
+ customDuration: duration,
90
+ fadeType,
91
+ isPaused: true,
92
+ isTransitioning: true,
93
+ originalVolume
94
+ };
95
+
96
+ if (effectiveDuration === 0) {
97
+ // Instant pause
98
+ await pauseChannel(channelNumber);
99
+ // Reset volume to original for resume (synchronously to avoid state issues)
100
+ setChannelVolumeSync(channelNumber, originalVolume);
101
+ // Mark transition as complete for instant pause
102
+ if (channel.fadeState) {
103
+ channel.fadeState.isTransitioning = false;
104
+ }
105
+ return;
106
+ }
107
+
108
+ // Fade to 0 with pause curve, then pause
109
+ await transitionVolume(channelNumber, 0, effectiveDuration, config.pauseCurve);
110
+ await pauseChannel(channelNumber);
111
+
112
+ // Reset volume to original for resume (synchronously to avoid state issues)
113
+ setChannelVolumeSync(channelNumber, originalVolume);
114
+
115
+ // Mark transition as complete
116
+ if (channel.fadeState) {
117
+ channel.fadeState.isTransitioning = false;
118
+ }
119
+ };
120
+
121
+ /**
122
+ * Resumes the currently paused audio in a specific channel with smooth volume fade
123
+ * Uses the complementary fade curve automatically based on the pause fade type, or allows override
124
+ * @param fadeType - Optional fade type to override the stored fade type from pause
125
+ * @param channelNumber - The channel number to resume (defaults to 0)
126
+ * @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
127
+ * @returns Promise that resolves when the resume and fade are complete
128
+ * @example
129
+ * ```typescript
130
+ * await resumeWithFade(); // Resume with automatically paired fade curve from pause
131
+ * await resumeWithFade(FadeType.Dramatic, 0); // Override with dramatic fade
132
+ * await resumeWithFade(FadeType.Linear, 0, 1000); // Override with linear fade over 1 second
133
+ * ```
134
+ */
135
+ export const resumeWithFade = async (
136
+ fadeType?: FadeType,
137
+ channelNumber: number = 0,
138
+ duration?: number
139
+ ): Promise<void> => {
140
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
141
+
142
+ if (!channel || channel.queue.length === 0) return;
143
+
144
+ const fadeState: ChannelFadeState | undefined = channel.fadeState;
145
+ if (!fadeState?.isPaused) {
146
+ // Fall back to regular resume if no fade state
147
+ await resumeChannel(channelNumber);
148
+ return;
149
+ }
150
+
151
+ // Use provided fadeType or fall back to stored fadeType from pause
152
+ const effectiveFadeType: FadeType = fadeType ?? fadeState.fadeType;
153
+ const config: FadeConfig = getFadeConfig(effectiveFadeType);
154
+
155
+ // Determine effective duration: custom parameter > stored custom > fadeType default
156
+ let effectiveDuration: number;
157
+ if (duration !== undefined) {
158
+ effectiveDuration = duration;
159
+ } else if (fadeState.customDuration !== undefined) {
160
+ effectiveDuration = fadeState.customDuration;
161
+ } else {
162
+ effectiveDuration = config.duration;
163
+ }
164
+
165
+ if (effectiveDuration === 0) {
166
+ // Instant resume
167
+ const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
168
+ setChannelVolumeSync(channelNumber, targetVolume);
169
+ await resumeChannel(channelNumber);
170
+ fadeState.isPaused = false;
171
+ fadeState.isTransitioning = false;
172
+ return;
173
+ }
174
+
175
+ // Race condition fix: Ensure we have a valid original volume to restore to
176
+ const targetVolume = fadeState.originalVolume > 0 ? fadeState.originalVolume : 1.0;
177
+
178
+ // Mark as transitioning to prevent volume capture during rapid toggles
179
+ fadeState.isTransitioning = true;
180
+
181
+ // Set volume to 0, resume, then fade to original with resume curve
182
+ setChannelVolumeSync(channelNumber, 0);
183
+ await resumeChannel(channelNumber);
184
+
185
+ // Use the stored original volume, not current volume, to prevent race conditions
186
+ await transitionVolume(channelNumber, targetVolume, effectiveDuration, config.resumeCurve);
187
+
188
+ fadeState.isPaused = false;
189
+ fadeState.isTransitioning = false;
190
+ };
191
+
192
+ /**
193
+ * Toggles pause/resume state for a specific channel with integrated fade
194
+ * @param fadeType - Type of fade transition to apply when pausing
195
+ * @param channelNumber - The channel number to toggle (defaults to 0)
196
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
197
+ * @returns Promise that resolves when the toggle and fade are complete
198
+ * @example
199
+ * ```typescript
200
+ * await togglePauseWithFade(FadeType.Gentle, 0); // Toggle with gentle fade
201
+ * await togglePauseWithFade(FadeType.Dramatic, 0, 500); // Toggle with custom 500ms fade
202
+ * ```
203
+ */
204
+ export const togglePauseWithFade = async (
205
+ fadeType: FadeType = FadeType.Gentle,
206
+ channelNumber: number = 0,
207
+ duration?: number
208
+ ): Promise<void> => {
209
+ const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
210
+
211
+ if (!channel || channel.queue.length === 0) return;
212
+
213
+ const currentAudio: HTMLAudioElement = channel.queue[0];
214
+
215
+ if (currentAudio.paused) {
216
+ await resumeWithFade(undefined, channelNumber, duration);
217
+ } else {
218
+ await pauseWithFade(fadeType, channelNumber, duration);
219
+ }
220
+ };
221
+
222
+ /**
223
+ * Pauses all currently playing audio across all channels with smooth volume fade
224
+ * @param fadeType - Type of fade transition to apply to all channels
225
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
226
+ * @returns Promise that resolves when all channels are paused and faded
227
+ * @example
228
+ * ```typescript
229
+ * await pauseAllWithFade(FadeType.Dramatic); // Pause everything with dramatic fade
230
+ * await pauseAllWithFade(FadeType.Gentle, 1200); // Pause all channels with custom 1.2s fade
231
+ * ```
232
+ */
233
+ export const pauseAllWithFade = async (
234
+ fadeType: FadeType = FadeType.Gentle,
235
+ duration?: number
236
+ ): Promise<void> => {
237
+ const pausePromises: Promise<void>[] = [];
238
+
239
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
240
+ pausePromises.push(pauseWithFade(fadeType, index, duration));
241
+ });
242
+
243
+ await Promise.all(pausePromises);
244
+ };
245
+
246
+ /**
247
+ * Resumes all currently paused audio across all channels with smooth volume fade
248
+ * Uses automatically paired fade curves based on each channel's pause fade type, or allows override
249
+ * @param fadeType - Optional fade type to override stored fade types for all channels
250
+ * @param duration - Optional custom fade duration in milliseconds (uses stored or fadeType default if not provided)
251
+ * @returns Promise that resolves when all channels are resumed and faded
252
+ * @example
253
+ * ```typescript
254
+ * await resumeAllWithFade(); // Resume everything with paired fade curves
255
+ * await resumeAllWithFade(FadeType.Gentle, 800); // Override all channels with gentle fade over 800ms
256
+ * await resumeAllWithFade(undefined, 600); // Use stored fade types with custom 600ms duration
257
+ * ```
258
+ */
259
+ export const resumeAllWithFade = async (fadeType?: FadeType, duration?: number): Promise<void> => {
260
+ const resumePromises: Promise<void>[] = [];
261
+
262
+ audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
263
+ resumePromises.push(resumeWithFade(fadeType, index, duration));
264
+ });
265
+
266
+ await Promise.all(resumePromises);
267
+ };
268
+
269
+ /**
270
+ * Toggles pause/resume state for all channels with integrated fade
271
+ * If any channels are playing, all will be paused with fade
272
+ * If all channels are paused, all will be resumed with fade
273
+ * @param fadeType - Type of fade transition to apply when pausing
274
+ * @param duration - Optional custom fade duration in milliseconds (uses fadeType default if not provided)
275
+ * @returns Promise that resolves when all toggles and fades are complete
276
+ * @example
277
+ * ```typescript
278
+ * await togglePauseAllWithFade(FadeType.Gentle); // Global toggle with gentle fade
279
+ * await togglePauseAllWithFade(FadeType.Dramatic, 600); // Global toggle with custom 600ms fade
280
+ * ```
281
+ */
282
+ export const togglePauseAllWithFade = async (
283
+ fadeType: FadeType = FadeType.Gentle,
284
+ duration?: number
285
+ ): Promise<void> => {
286
+ let hasPlayingChannel: boolean = false;
287
+
288
+ // Check if any channel is currently playing
289
+ for (let i: number = 0; i < audioChannels.length; i++) {
290
+ const channel: ExtendedAudioQueueChannel = audioChannels[i];
291
+ if (channel && channel.queue.length > 0) {
292
+ const currentAudio: HTMLAudioElement = channel.queue[0];
293
+ if (!currentAudio.paused && !currentAudio.ended) {
294
+ hasPlayingChannel = true;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ // If any channel is playing, pause all with fade
301
+ // If no channels are playing, resume all with fade
302
+ if (hasPlayingChannel) {
303
+ await pauseAllWithFade(fadeType, duration);
304
+ } else {
305
+ await resumeAllWithFade(fadeType, duration);
306
+ }
307
+ };
9
308
 
10
309
  /**
11
310
  * Pauses the currently playing audio in a specific channel
@@ -19,15 +318,19 @@ import { emitAudioPause, emitAudioResume } from './events';
19
318
  */
20
319
  export const pauseChannel = async (channelNumber: number = 0): Promise<void> => {
21
320
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
22
-
321
+
23
322
  if (channel && channel.queue.length > 0) {
24
323
  const currentAudio: HTMLAudioElement = channel.queue[0];
25
-
324
+
26
325
  if (!currentAudio.paused && !currentAudio.ended) {
27
326
  currentAudio.pause();
28
327
  channel.isPaused = true;
29
-
30
- const audioInfo: AudioInfo | null = getAudioInfoFromElement(currentAudio, channelNumber, audioChannels);
328
+
329
+ const audioInfo: AudioInfo | null = getAudioInfoFromElement(
330
+ currentAudio,
331
+ channelNumber,
332
+ audioChannels
333
+ );
31
334
  if (audioInfo) {
32
335
  emitAudioPause(channelNumber, audioInfo, audioChannels);
33
336
  }
@@ -47,16 +350,20 @@ export const pauseChannel = async (channelNumber: number = 0): Promise<void> =>
47
350
  */
48
351
  export const resumeChannel = async (channelNumber: number = 0): Promise<void> => {
49
352
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
50
-
353
+
51
354
  if (channel && channel.queue.length > 0) {
52
355
  const currentAudio: HTMLAudioElement = channel.queue[0];
53
-
356
+
54
357
  // Only resume if both the channel is marked as paused AND the audio element is actually paused AND not ended
55
358
  if (channel.isPaused && currentAudio.paused && !currentAudio.ended) {
56
359
  await currentAudio.play();
57
360
  channel.isPaused = false;
58
-
59
- const audioInfo: AudioInfo | null = getAudioInfoFromElement(currentAudio, channelNumber, audioChannels);
361
+
362
+ const audioInfo: AudioInfo | null = getAudioInfoFromElement(
363
+ currentAudio,
364
+ channelNumber,
365
+ audioChannels
366
+ );
60
367
  if (audioInfo) {
61
368
  emitAudioResume(channelNumber, audioInfo, audioChannels);
62
369
  }
@@ -75,10 +382,10 @@ export const resumeChannel = async (channelNumber: number = 0): Promise<void> =>
75
382
  */
76
383
  export const togglePauseChannel = async (channelNumber: number = 0): Promise<void> => {
77
384
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
78
-
385
+
79
386
  if (channel && channel.queue.length > 0) {
80
387
  const currentAudio: HTMLAudioElement = channel.queue[0];
81
-
388
+
82
389
  if (currentAudio.paused) {
83
390
  await resumeChannel(channelNumber);
84
391
  } else {
@@ -97,11 +404,11 @@ export const togglePauseChannel = async (channelNumber: number = 0): Promise<voi
97
404
  */
98
405
  export const pauseAllChannels = async (): Promise<void> => {
99
406
  const pausePromises: Promise<void>[] = [];
100
-
407
+
101
408
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
102
409
  pausePromises.push(pauseChannel(index));
103
410
  });
104
-
411
+
105
412
  await Promise.all(pausePromises);
106
413
  };
107
414
 
@@ -115,11 +422,11 @@ export const pauseAllChannels = async (): Promise<void> => {
115
422
  */
116
423
  export const resumeAllChannels = async (): Promise<void> => {
117
424
  const resumePromises: Promise<void>[] = [];
118
-
425
+
119
426
  audioChannels.forEach((_channel: ExtendedAudioQueueChannel, index: number) => {
120
427
  resumePromises.push(resumeChannel(index));
121
428
  });
122
-
429
+
123
430
  await Promise.all(resumePromises);
124
431
  };
125
432
 
@@ -135,7 +442,7 @@ export const resumeAllChannels = async (): Promise<void> => {
135
442
  */
136
443
  export const isChannelPaused = (channelNumber: number = 0): boolean => {
137
444
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
138
- return channel?.isPaused || false;
445
+ return channel?.isPaused ?? false;
139
446
  };
140
447
 
141
448
  /**
@@ -150,9 +457,7 @@ export const isChannelPaused = (channelNumber: number = 0): boolean => {
150
457
  * ```
151
458
  */
152
459
  export const getAllChannelsPauseState = (): boolean[] => {
153
- return audioChannels.map((channel: ExtendedAudioQueueChannel) =>
154
- channel?.isPaused || false
155
- );
460
+ return audioChannels.map((channel: ExtendedAudioQueueChannel) => channel?.isPaused ?? false);
156
461
  };
157
462
 
158
463
  /**
@@ -167,9 +472,9 @@ export const getAllChannelsPauseState = (): boolean[] => {
167
472
  */
168
473
  export const togglePauseAllChannels = async (): Promise<void> => {
169
474
  let hasPlayingChannel: boolean = false;
170
-
475
+
171
476
  // Check if any channel is currently playing
172
- for (let i = 0; i < audioChannels.length; i++) {
477
+ for (let i: number = 0; i < audioChannels.length; i++) {
173
478
  const channel: ExtendedAudioQueueChannel = audioChannels[i];
174
479
  if (channel && channel.queue.length > 0) {
175
480
  const currentAudio: HTMLAudioElement = channel.queue[0];
@@ -179,7 +484,7 @@ export const togglePauseAllChannels = async (): Promise<void> => {
179
484
  }
180
485
  }
181
486
  }
182
-
487
+
183
488
  // If any channel is playing, pause all channels
184
489
  // If no channels are playing, resume all channels
185
490
  if (hasPlayingChannel) {
@@ -187,4 +492,4 @@ export const togglePauseAllChannels = async (): Promise<void> => {
187
492
  } else {
188
493
  await resumeAllChannels();
189
494
  }
190
- };
495
+ };
package/src/types.ts CHANGED
@@ -2,6 +2,12 @@
2
2
  * @fileoverview Type definitions for the audio-channel-queue package
3
3
  */
4
4
 
5
+ /**
6
+ * Symbol used as a key for global (channel-wide) progress callbacks
7
+ * This avoids the need for `null as any` type assertions
8
+ */
9
+ export const GLOBAL_PROGRESS_KEY: unique symbol = Symbol('global-progress-callbacks');
10
+
5
11
  /**
6
12
  * Array of HTMLAudioElement objects representing an audio queue
7
13
  */
@@ -10,7 +16,7 @@ export type AudioQueue = HTMLAudioElement[];
10
16
  /**
11
17
  * Basic audio queue channel structure
12
18
  */
13
- export type AudioQueueChannel = {
19
+ export interface AudioQueueChannel {
14
20
  queue: AudioQueue;
15
21
  }
16
22
 
@@ -29,7 +35,7 @@ export interface VolumeConfig {
29
35
  /** Duration in milliseconds for volume restore transition (defaults to 500ms) */
30
36
  restoreTransitionDuration?: number;
31
37
  /** Easing function for volume transitions (defaults to 'ease-out') */
32
- transitionEasing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
38
+ transitionEasing?: EasingType;
33
39
  }
34
40
 
35
41
  /**
@@ -226,11 +232,59 @@ export interface ExtendedAudioQueueChannel {
226
232
  audioPauseCallbacks: Set<AudioPauseCallback>;
227
233
  audioResumeCallbacks: Set<AudioResumeCallback>;
228
234
  audioStartCallbacks: Set<AudioStartCallback>;
235
+ fadeState?: ChannelFadeState;
229
236
  isPaused?: boolean;
230
- progressCallbacks: Map<HTMLAudioElement | null, Set<ProgressCallback>>;
237
+ progressCallbacks: Map<HTMLAudioElement | typeof GLOBAL_PROGRESS_KEY, Set<ProgressCallback>>;
231
238
  queue: HTMLAudioElement[];
232
239
  queueChangeCallbacks: Set<QueueChangeCallback>;
233
240
  retryConfig?: RetryConfig;
234
241
  volume?: number;
235
242
  volumeConfig?: VolumeConfig;
243
+ }
244
+
245
+ /**
246
+ * Easing function types for volume transitions
247
+ */
248
+ export enum EasingType {
249
+ Linear = 'linear',
250
+ EaseIn = 'ease-in',
251
+ EaseOut = 'ease-out',
252
+ EaseInOut = 'ease-in-out'
253
+ }
254
+
255
+ /**
256
+ * Fade type for pause/resume operations with integrated volume transitions
257
+ */
258
+ export enum FadeType {
259
+ Linear = 'linear',
260
+ Gentle = 'gentle',
261
+ Dramatic = 'dramatic'
262
+ }
263
+
264
+ /**
265
+ * Configuration for fade transitions
266
+ */
267
+ export interface FadeConfig {
268
+ /** Duration in milliseconds for the fade transition */
269
+ duration: number;
270
+ /** Easing curve to use when pausing (fading out) */
271
+ pauseCurve: EasingType;
272
+ /** Easing curve to use when resuming (fading in) */
273
+ resumeCurve: EasingType;
274
+ }
275
+
276
+ /**
277
+ * Internal fade state tracking for pause/resume with fade functionality
278
+ */
279
+ export interface ChannelFadeState {
280
+ /** The original volume level before fading began */
281
+ originalVolume: number;
282
+ /** The type of fade being used */
283
+ fadeType: FadeType;
284
+ /** Whether the channel is currently paused due to fade */
285
+ isPaused: boolean;
286
+ /** Custom duration in milliseconds if specified (overrides fade type default) */
287
+ customDuration?: number;
288
+ /** Whether the channel is currently transitioning (during any fade operation) to prevent capturing intermediate volumes during rapid pause/resume toggles */
289
+ isTransitioning?: boolean;
236
290
  }
package/src/utils.ts CHANGED
@@ -40,14 +40,14 @@ export const extractFileName = (url: string): string => {
40
40
  * const audioElement = new Audio('song.mp3');
41
41
  * const info = getAudioInfoFromElement(audioElement);
42
42
  * console.log(info?.progress); // Current progress as decimal (0-1)
43
- *
43
+ *
44
44
  * // With channel context for remainingInQueue
45
45
  * const infoWithQueue = getAudioInfoFromElement(audioElement, 0, audioChannels);
46
46
  * console.log(infoWithQueue?.remainingInQueue); // Number of items left in queue
47
47
  * ```
48
48
  */
49
49
  export const getAudioInfoFromElement = (
50
- audio: HTMLAudioElement,
50
+ audio: HTMLAudioElement,
51
51
  channelNumber?: number,
52
52
  audioChannels?: ExtendedAudioQueueChannel[]
53
53
  ): AudioInfo | null => {
@@ -60,7 +60,7 @@ export const getAudioInfoFromElement = (
60
60
 
61
61
  // Calculate remainingInQueue if channel context is provided
62
62
  let remainingInQueue: number = 0;
63
- if (channelNumber !== undefined && audioChannels && audioChannels[channelNumber]) {
63
+ if (channelNumber !== undefined && audioChannels?.[channelNumber]) {
64
64
  const channel = audioChannels[channelNumber];
65
65
  remainingInQueue = Math.max(0, channel.queue.length - 1); // Exclude current playing audio
66
66
  }
@@ -91,7 +91,7 @@ export const getAudioInfoFromElement = (
91
91
  * ```
92
92
  */
93
93
  export const createQueueSnapshot = (
94
- channelNumber: number,
94
+ channelNumber: number,
95
95
  audioChannels: ExtendedAudioQueueChannel[]
96
96
  ): QueueSnapshot | null => {
97
97
  const channel: ExtendedAudioQueueChannel = audioChannels[channelNumber];
@@ -109,10 +109,10 @@ export const createQueueSnapshot = (
109
109
  return {
110
110
  channelNumber,
111
111
  currentIndex: 0, // Current playing is always index 0 in our queue structure
112
- isPaused: channel.isPaused || false,
112
+ isPaused: channel.isPaused ?? false,
113
113
  items,
114
114
  totalItems: channel.queue.length,
115
- volume: channel.volume || 1.0
115
+ volume: channel.volume ?? 1.0
116
116
  };
117
117
  };
118
118
 
@@ -131,4 +131,4 @@ export const createQueueSnapshot = (
131
131
  export const cleanWebpackFilename = (fileName: string): string => {
132
132
  // Remove webpack hash pattern: filename.hash.ext → filename.ext
133
133
  return fileName.replace(/\.[a-f0-9]{8,}\./i, '.');
134
- };
134
+ };