@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 +1 -1
- package/src/AudioManager.ts +147 -20
- package/src/index.ts +2 -1
- package/src/types.ts +16 -0
- package/src/useSound.ts +38 -3
package/package.json
CHANGED
package/src/AudioManager.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { Audio, AVPlaybackStatus, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
|
|
2
2
|
import { useSoundStore } from './store';
|
|
3
|
-
import {
|
|
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 (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
seek,
|
|
52
82
|
setVolume,
|
|
83
|
+
setRate,
|
|
84
|
+
preload,
|
|
85
|
+
unload,
|
|
86
|
+
clearCache,
|
|
87
|
+
isCached,
|
|
53
88
|
isPlaying,
|
|
54
89
|
isBuffering,
|
|
55
90
|
position,
|