@umituz/react-native-sound 1.2.12 → 1.2.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-sound",
3
- "version": "1.2.12",
3
+ "version": "1.2.13",
4
4
  "description": "Universal sound playback and caching library for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,10 +1,26 @@
1
1
  import { Audio, AVPlaybackStatus, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
2
2
  import { useSoundStore } from './store';
3
- import { PlaybackOptions, SoundSource, isPlaybackStatusSuccess, isSoundSourceValid } from './types';
3
+ import {
4
+ PlaybackOptions,
5
+ SoundSource,
6
+ isPlaybackStatusSuccess,
7
+ isSoundSourceValid,
8
+ clampVolume,
9
+ clampRate,
10
+ } from './types';
11
+
12
+ interface CachedSound {
13
+ sound: Audio.Sound;
14
+ source: SoundSource;
15
+ loadedAt: number;
16
+ }
4
17
 
5
18
  class AudioManager {
6
19
  private sound: Audio.Sound | null = null;
7
20
  private currentId: string | null = null;
21
+ private cache: Map<string, CachedSound> = new Map();
22
+ private readonly maxCacheSize = 3;
23
+ private readonly cacheExpireMs = 5 * 60 * 1000;
8
24
 
9
25
  constructor() {
10
26
  this.configureAudio();
@@ -46,6 +62,66 @@ class AudioManager {
46
62
  }
47
63
  };
48
64
 
65
+ private cleanExpiredCache() {
66
+ const now = Date.now();
67
+ for (const [id, cached] of this.cache) {
68
+ if (now - cached.loadedAt > this.cacheExpireMs) {
69
+ cached.sound.unloadAsync().catch(() => {});
70
+ this.cache.delete(id);
71
+ if (__DEV__) console.log('[AudioManager] Expired cache removed:', id);
72
+ }
73
+ }
74
+ }
75
+
76
+ private enforceCacheLimit() {
77
+ if (this.cache.size >= this.maxCacheSize) {
78
+ const firstKey = this.cache.keys().next().value;
79
+ if (firstKey) {
80
+ const cached = this.cache.get(firstKey);
81
+ cached?.sound.unloadAsync().catch(() => {});
82
+ this.cache.delete(firstKey);
83
+ if (__DEV__) console.log('[AudioManager] Cache limit enforced, removed:', firstKey);
84
+ }
85
+ }
86
+ }
87
+
88
+ async preload(id: string, source: SoundSource, options?: PlaybackOptions) {
89
+ if (!isSoundSourceValid(source)) {
90
+ throw new Error('Invalid sound source: source is null or undefined');
91
+ }
92
+
93
+ if (this.cache.has(id)) {
94
+ if (__DEV__) console.log('[AudioManager] Already cached:', id);
95
+ return;
96
+ }
97
+
98
+ try {
99
+ this.cleanExpiredCache();
100
+ this.enforceCacheLimit();
101
+
102
+ const volume = options?.volume !== undefined ? clampVolume(options.volume) : 1.0;
103
+ const rate = options?.rate !== undefined ? clampRate(options.rate) : 1.0;
104
+
105
+ const { sound } = await Audio.Sound.createAsync(
106
+ source,
107
+ {
108
+ shouldPlay: false,
109
+ isLooping: options?.isLooping ?? false,
110
+ volume,
111
+ rate,
112
+ },
113
+ this.onPlaybackStatusUpdate
114
+ );
115
+
116
+ this.cache.set(id, { sound, source, loadedAt: Date.now() });
117
+ if (__DEV__) console.log('[AudioManager] Preloaded:', id);
118
+ } catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
120
+ if (__DEV__) console.error('[AudioManager] Error preloading sound:', error);
121
+ throw new Error(errorMessage);
122
+ }
123
+ }
124
+
49
125
  async play(id: string, source: SoundSource, options?: PlaybackOptions) {
50
126
  const store = useSoundStore.getState();
51
127
 
@@ -73,26 +149,41 @@ class AudioManager {
73
149
  try {
74
150
  await this.unload();
75
151
 
152
+ const volume = options?.volume !== undefined ? clampVolume(options.volume) : 1.0;
153
+ const rate = options?.rate !== undefined ? clampRate(options.rate) : 1.0;
154
+
155
+ const cached = this.cache.get(id);
156
+ if (cached) {
157
+ this.sound = cached.sound;
158
+ this.cache.delete(id);
159
+ if (__DEV__) console.log('[AudioManager] Using cached sound:', id);
160
+ } else {
161
+ const { sound } = await Audio.Sound.createAsync(
162
+ source,
163
+ {
164
+ shouldPlay: true,
165
+ isLooping: options?.isLooping ?? false,
166
+ volume,
167
+ rate,
168
+ positionMillis: options?.positionMillis ?? 0,
169
+ },
170
+ this.onPlaybackStatusUpdate
171
+ );
172
+ this.sound = sound;
173
+ if (__DEV__) console.log('[AudioManager] Sound created and playing');
174
+ }
175
+
76
176
  this.currentId = id;
77
177
  store.setCurrent(id, source);
78
178
  store.setError(null);
79
179
 
80
- if (__DEV__) console.log('[AudioManager] Creating sound from source:', source);
81
-
82
- const { sound } = await Audio.Sound.createAsync(
83
- source,
84
- {
85
- shouldPlay: true,
86
- isLooping: options?.isLooping ?? false,
87
- volume: options?.volume ?? 1.0,
88
- rate: options?.rate ?? 1.0,
89
- positionMillis: options?.positionMillis ?? 0,
90
- },
91
- this.onPlaybackStatusUpdate
92
- );
93
-
94
- this.sound = sound;
95
- if (__DEV__) console.log('[AudioManager] Sound created and playing');
180
+ if (!cached) {
181
+ await this.sound?.playAsync();
182
+ } else {
183
+ await this.sound?.setVolumeAsync(volume);
184
+ await this.sound?.setRateAsync(rate, false);
185
+ await this.sound?.playAsync();
186
+ }
96
187
  } catch (error) {
97
188
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
98
189
  if (__DEV__) console.error('[AudioManager] Error playing sound:', error);
@@ -122,10 +213,34 @@ class AudioManager {
122
213
  }
123
214
  }
124
215
 
216
+ async seek(positionMillis: number) {
217
+ if (this.sound) {
218
+ const status = await this.sound.getStatusAsync();
219
+ if (status.isLoaded) {
220
+ await this.sound.setStatusAsync({ positionMillis: Math.max(0, positionMillis) });
221
+ }
222
+ }
223
+ }
224
+
125
225
  async setVolume(volume: number) {
126
226
  if (this.sound) {
127
- await this.sound.setVolumeAsync(volume);
128
- useSoundStore.getState().setVolumeState(volume);
227
+ const clampedVolume = clampVolume(volume);
228
+ await this.sound.setVolumeAsync(clampedVolume);
229
+ useSoundStore.getState().setVolumeState(clampedVolume);
230
+ }
231
+ }
232
+
233
+ async setRate(rate: number) {
234
+ if (this.sound) {
235
+ const clampedRate = clampRate(rate);
236
+ const status = await this.sound.getStatusAsync();
237
+ if (status.isLoaded) {
238
+ try {
239
+ await this.sound.setRateAsync(clampedRate, false);
240
+ } catch (error) {
241
+ if (__DEV__) console.warn('[AudioManager] Could not set rate:', error);
242
+ }
243
+ }
129
244
  }
130
245
  }
131
246
 
@@ -134,7 +249,7 @@ class AudioManager {
134
249
  try {
135
250
  await this.sound.unloadAsync();
136
251
  } catch (e) {
137
- // Ignore unload errors
252
+ if (__DEV__) console.warn('[AudioManager] Error unloading sound:', e);
138
253
  }
139
254
  this.sound = null;
140
255
  }
@@ -142,9 +257,21 @@ class AudioManager {
142
257
  useSoundStore.getState().reset();
143
258
  }
144
259
 
260
+ clearCache() {
261
+ for (const cached of this.cache.values()) {
262
+ cached.sound.unloadAsync().catch(() => {});
263
+ }
264
+ this.cache.clear();
265
+ if (__DEV__) console.log('[AudioManager] Cache cleared');
266
+ }
267
+
145
268
  getCurrentId() {
146
269
  return this.currentId;
147
270
  }
271
+
272
+ isCached(id: string): boolean {
273
+ return this.cache.has(id);
274
+ }
148
275
  }
149
276
 
150
277
  export const audioManager = new AudioManager();
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @umituz/react-native-sound
3
- * Universal sound playback library
3
+ * Universal sound playback library with caching
4
4
  */
5
5
 
6
6
  export * from './types';
7
7
  export * from './useSound';
8
8
  export { audioManager } from './AudioManager';
9
+ export { useSoundStore } from './store';
package/src/types.ts CHANGED
@@ -43,3 +43,19 @@ export function isPlaybackStatusSuccess(status: AVPlaybackStatus): status is Pla
43
43
  export function isSoundSourceValid(source: SoundSource): source is number | { uri: string; headers?: Record<string, string> } {
44
44
  return source !== null && source !== undefined;
45
45
  }
46
+
47
+ export function validateVolume(volume: number): boolean {
48
+ return typeof volume === 'number' && volume >= 0 && volume <= 1;
49
+ }
50
+
51
+ export function validateRate(rate: number): boolean {
52
+ return typeof rate === 'number' && rate >= 0.5 && rate <= 2.0;
53
+ }
54
+
55
+ export function clampVolume(volume: number): number {
56
+ return Math.max(0, Math.min(1, volume));
57
+ }
58
+
59
+ export function clampRate(rate: number): number {
60
+ return Math.max(0.5, Math.min(2.0, rate));
61
+ }
package/src/useSound.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useEffect } from 'react';
2
2
  import { useSoundStore } from './store';
3
3
  import { audioManager } from './AudioManager';
4
4
  import { PlaybackOptions, SoundSource } from './types';
@@ -12,9 +12,14 @@ export const useSound = () => {
12
12
  const error = useSoundStore((state) => state.error);
13
13
  const volume = useSoundStore((state) => state.volume);
14
14
 
15
+ useEffect(() => {
16
+ return () => {
17
+ if (__DEV__) console.log('[useSound] Cleanup on unmount');
18
+ };
19
+ }, []);
20
+
15
21
  const play = useCallback(
16
22
  async (id: string, source: SoundSource, options?: PlaybackOptions) => {
17
- // Toggle logic: If same ID, toggle pause/resume
18
23
  if (currentId === id) {
19
24
  if (isPlaying) {
20
25
  await audioManager.pause();
@@ -32,24 +37,54 @@ export const useSound = () => {
32
37
  await audioManager.pause();
33
38
  }, []);
34
39
 
40
+ const resume = useCallback(async () => {
41
+ await audioManager.resume();
42
+ }, []);
43
+
35
44
  const stop = useCallback(async () => {
36
45
  await audioManager.stop();
37
46
  }, []);
38
47
 
48
+ const seek = useCallback(async (positionMillis: number) => {
49
+ await audioManager.seek(positionMillis);
50
+ }, []);
51
+
39
52
  const setVolume = useCallback(async (vol: number) => {
40
53
  await audioManager.setVolume(vol);
41
54
  }, []);
42
55
 
56
+ const setRate = useCallback(async (rate: number) => {
57
+ await audioManager.setRate(rate);
58
+ }, []);
59
+
60
+ const preload = useCallback(async (id: string, source: SoundSource, options?: PlaybackOptions) => {
61
+ await audioManager.preload(id, source, options);
62
+ }, []);
63
+
43
64
  const unload = useCallback(async () => {
44
65
  await audioManager.unload();
45
66
  }, []);
46
67
 
68
+ const clearCache = useCallback(() => {
69
+ audioManager.clearCache();
70
+ }, []);
71
+
72
+ const isCached = useCallback((id: string): boolean => {
73
+ return audioManager.isCached(id);
74
+ }, []);
75
+
47
76
  return {
48
77
  play,
49
78
  pause,
79
+ resume,
50
80
  stop,
51
- unload,
81
+ seek,
52
82
  setVolume,
83
+ setRate,
84
+ preload,
85
+ unload,
86
+ clearCache,
87
+ isCached,
53
88
  isPlaying,
54
89
  isBuffering,
55
90
  position,