@umituz/react-native-sound 1.2.20 → 1.2.21
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 +39 -3
- package/src/AudioManager.ts +98 -37
- package/src/global.d.ts +5 -0
- package/src/index.ts +9 -7
- package/src/store.ts +5 -0
- package/src/types.ts +2 -36
- package/src/useSound.ts +29 -18
- package/src/utils.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-sound",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.21",
|
|
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",
|
|
@@ -32,13 +32,49 @@
|
|
|
32
32
|
"react-native": ">=0.74.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@gorhom/bottom-sheet": "^5.2.8",
|
|
35
36
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
37
|
+
"@react-navigation/bottom-tabs": "^7.15.5",
|
|
38
|
+
"@react-navigation/elements": "^2.9.10",
|
|
39
|
+
"@react-navigation/native": "^7.1.33",
|
|
40
|
+
"@react-navigation/stack": "^7.8.5",
|
|
41
|
+
"@tanstack/react-query": "^5.90.21",
|
|
42
|
+
"@types/node": "^25.5.0",
|
|
36
43
|
"@types/react": "~19.1.10",
|
|
37
|
-
"@
|
|
44
|
+
"@types/react-native": "^0.72.8",
|
|
45
|
+
"@types/uuid": "^10.0.0",
|
|
46
|
+
"@umituz/react-native-design-system": "^4.25.97",
|
|
47
|
+
"@umituz/react-native-settings": "^5.3.48",
|
|
48
|
+
"expo-application": "^55.0.9",
|
|
49
|
+
"expo-auth-session": "^55.0.8",
|
|
38
50
|
"expo-av": "~13.4.1",
|
|
39
|
-
"
|
|
51
|
+
"expo-clipboard": "^55.0.8",
|
|
52
|
+
"expo-crypto": "^55.0.9",
|
|
53
|
+
"expo-device": "^55.0.9",
|
|
54
|
+
"expo-file-system": "^55.0.10",
|
|
55
|
+
"expo-font": "^55.0.4",
|
|
56
|
+
"expo-haptics": "^55.0.8",
|
|
57
|
+
"expo-image": "^55.0.6",
|
|
58
|
+
"expo-image-manipulator": "^55.0.10",
|
|
59
|
+
"expo-image-picker": "^55.0.12",
|
|
60
|
+
"expo-localization": "^55.0.8",
|
|
61
|
+
"expo-media-library": "^55.0.9",
|
|
62
|
+
"expo-modules-core": "^55.0.15",
|
|
63
|
+
"expo-network": "^55.0.8",
|
|
64
|
+
"expo-secure-store": "^55.0.8",
|
|
65
|
+
"expo-sharing": "^55.0.11",
|
|
66
|
+
"expo-video": "^55.0.10",
|
|
67
|
+
"expo-web-browser": "^55.0.9",
|
|
68
|
+
"i18next": "^25.8.18",
|
|
69
|
+
"react": "^19.1.0",
|
|
70
|
+
"react-i18next": "^16.5.8",
|
|
40
71
|
"react-native": "0.81.5",
|
|
72
|
+
"react-native-gesture-handler": "^2.30.0",
|
|
73
|
+
"react-native-safe-area-context": "^5.7.0",
|
|
74
|
+
"react-native-svg": "^15.15.3",
|
|
75
|
+
"rn-emoji-keyboard": "^1.7.0",
|
|
41
76
|
"typescript": "~5.9.2",
|
|
77
|
+
"uuid": "^13.0.0",
|
|
42
78
|
"zustand": "^5.0.9"
|
|
43
79
|
},
|
|
44
80
|
"publishConfig": {
|
package/src/AudioManager.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Audio, AVPlaybackStatus, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
|
|
2
2
|
import { useSoundStore } from './store';
|
|
3
|
+
import { PlaybackOptions, SoundSource } from './types';
|
|
3
4
|
import {
|
|
4
|
-
PlaybackOptions,
|
|
5
|
-
SoundSource,
|
|
6
5
|
isPlaybackStatusSuccess,
|
|
7
6
|
isSoundSourceValid,
|
|
8
7
|
clampVolume,
|
|
9
8
|
clampRate,
|
|
10
|
-
|
|
9
|
+
validateSoundId,
|
|
10
|
+
} from './utils';
|
|
11
11
|
|
|
12
12
|
interface CachedSound {
|
|
13
13
|
sound: Audio.Sound;
|
|
@@ -23,7 +23,9 @@ class AudioManager {
|
|
|
23
23
|
private readonly cacheExpireMs = 5 * 60 * 1000;
|
|
24
24
|
|
|
25
25
|
constructor() {
|
|
26
|
-
this.configureAudio()
|
|
26
|
+
this.configureAudio().catch((error) => {
|
|
27
|
+
if (__DEV__) console.error('[AudioManager] Failed to configure audio on initialization:', error);
|
|
28
|
+
});
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
private async configureAudio() {
|
|
@@ -56,6 +58,10 @@ class AudioManager {
|
|
|
56
58
|
store.setBuffering(status.isBuffering);
|
|
57
59
|
store.setProgress(status.positionMillis, status.durationMillis || 0);
|
|
58
60
|
|
|
61
|
+
if (status.isLoaded) {
|
|
62
|
+
store.setRateState(status.rate || 1.0);
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
if (status.didJustFinish && !status.isLooping) {
|
|
60
66
|
store.setPlaying(false);
|
|
61
67
|
store.setProgress(status.durationMillis || 0, status.durationMillis || 0);
|
|
@@ -66,7 +72,9 @@ class AudioManager {
|
|
|
66
72
|
const now = Date.now();
|
|
67
73
|
for (const [id, cached] of this.cache) {
|
|
68
74
|
if (now - cached.loadedAt > this.cacheExpireMs) {
|
|
69
|
-
cached.sound.unloadAsync().catch(() => {
|
|
75
|
+
cached.sound.unloadAsync().catch((error) => {
|
|
76
|
+
if (__DEV__) console.warn('[AudioManager] Failed to unload expired cache:', id, error);
|
|
77
|
+
});
|
|
70
78
|
this.cache.delete(id);
|
|
71
79
|
if (__DEV__) console.log('[AudioManager] Expired cache removed:', id);
|
|
72
80
|
}
|
|
@@ -78,14 +86,20 @@ class AudioManager {
|
|
|
78
86
|
const firstKey = this.cache.keys().next().value;
|
|
79
87
|
if (firstKey) {
|
|
80
88
|
const cached = this.cache.get(firstKey);
|
|
81
|
-
cached?.sound.unloadAsync().catch(() => {
|
|
89
|
+
cached?.sound.unloadAsync().catch((error) => {
|
|
90
|
+
if (__DEV__) console.warn('[AudioManager] Failed to unload sound during cache limit enforcement:', firstKey, error);
|
|
91
|
+
});
|
|
82
92
|
this.cache.delete(firstKey);
|
|
83
93
|
if (__DEV__) console.log('[AudioManager] Cache limit enforced, removed:', firstKey);
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
async preload(id: string, source: SoundSource, options?: PlaybackOptions) {
|
|
98
|
+
async preload(id: string, source: SoundSource, options?: PlaybackOptions): Promise<void> {
|
|
99
|
+
if (!validateSoundId(id)) {
|
|
100
|
+
throw new Error('Invalid sound id: id must be a non-empty string');
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
if (!isSoundSourceValid(source)) {
|
|
90
104
|
throw new Error('Invalid sound source: source is null or undefined');
|
|
91
105
|
}
|
|
@@ -122,11 +136,17 @@ class AudioManager {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
async play(id: string, source: SoundSource, options?: PlaybackOptions) {
|
|
139
|
+
async play(id: string, source: SoundSource, options?: PlaybackOptions): Promise<void> {
|
|
126
140
|
const store = useSoundStore.getState();
|
|
127
141
|
|
|
128
142
|
if (__DEV__) console.log('[AudioManager] Play called with ID:', id);
|
|
129
143
|
|
|
144
|
+
if (!validateSoundId(id)) {
|
|
145
|
+
const errorMsg = 'Invalid sound id: id must be a non-empty string';
|
|
146
|
+
store.setError(errorMsg);
|
|
147
|
+
throw new Error(errorMsg);
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
if (!isSoundSourceValid(source)) {
|
|
131
151
|
const errorMsg = 'Invalid sound source: source is null or undefined';
|
|
132
152
|
store.setError(errorMsg);
|
|
@@ -137,9 +157,12 @@ class AudioManager {
|
|
|
137
157
|
const status = await this.sound.getStatusAsync();
|
|
138
158
|
if (status.isLoaded) {
|
|
139
159
|
if (status.isPlaying) {
|
|
160
|
+
store.setPlaying(false);
|
|
161
|
+
await this.sound.pauseAsync();
|
|
140
162
|
return;
|
|
141
163
|
} else {
|
|
142
164
|
if (__DEV__) console.log('[AudioManager] Resuming existing sound');
|
|
165
|
+
store.setPlaying(true);
|
|
143
166
|
await this.sound.playAsync();
|
|
144
167
|
return;
|
|
145
168
|
}
|
|
@@ -151,6 +174,7 @@ class AudioManager {
|
|
|
151
174
|
|
|
152
175
|
const volume = options?.volume !== undefined ? clampVolume(options.volume) : 1.0;
|
|
153
176
|
const rate = options?.rate !== undefined ? clampRate(options.rate) : 1.0;
|
|
177
|
+
const isLooping = options?.isLooping ?? false;
|
|
154
178
|
|
|
155
179
|
const cached = this.cache.get(id);
|
|
156
180
|
if (cached) {
|
|
@@ -162,7 +186,7 @@ class AudioManager {
|
|
|
162
186
|
source,
|
|
163
187
|
{
|
|
164
188
|
shouldPlay: true,
|
|
165
|
-
isLooping
|
|
189
|
+
isLooping,
|
|
166
190
|
volume,
|
|
167
191
|
rate,
|
|
168
192
|
positionMillis: options?.positionMillis ?? 0,
|
|
@@ -176,53 +200,88 @@ class AudioManager {
|
|
|
176
200
|
this.currentId = id;
|
|
177
201
|
store.setCurrent(id, source);
|
|
178
202
|
store.setError(null);
|
|
203
|
+
store.setLooping(isLooping);
|
|
204
|
+
store.setRateState(rate);
|
|
205
|
+
store.setVolumeState(volume);
|
|
179
206
|
|
|
180
|
-
|
|
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
|
-
}
|
|
207
|
+
await this.sound?.playAsync();
|
|
187
208
|
} catch (error) {
|
|
188
209
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
189
210
|
if (__DEV__) console.error('[AudioManager] Error playing sound:', error);
|
|
190
211
|
store.setError(errorMessage);
|
|
191
212
|
this.currentId = null;
|
|
213
|
+
this.sound = null;
|
|
192
214
|
throw error;
|
|
193
215
|
}
|
|
194
216
|
}
|
|
195
217
|
|
|
196
|
-
async pause() {
|
|
218
|
+
async pause(): Promise<void> {
|
|
197
219
|
if (this.sound) {
|
|
198
|
-
|
|
220
|
+
try {
|
|
221
|
+
await this.sound.pauseAsync();
|
|
222
|
+
useSoundStore.getState().setPlaying(false);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to pause sound';
|
|
225
|
+
if (__DEV__) console.error('[AudioManager] Error pausing sound:', error);
|
|
226
|
+
useSoundStore.getState().setError(errorMessage);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
199
229
|
}
|
|
200
230
|
}
|
|
201
231
|
|
|
202
|
-
async resume() {
|
|
232
|
+
async resume(): Promise<void> {
|
|
203
233
|
if (this.sound) {
|
|
204
|
-
|
|
234
|
+
try {
|
|
235
|
+
await this.sound.playAsync();
|
|
236
|
+
useSoundStore.getState().setPlaying(true);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to resume sound';
|
|
239
|
+
if (__DEV__) console.error('[AudioManager] Error resuming sound:', error);
|
|
240
|
+
useSoundStore.getState().setError(errorMessage);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
205
243
|
}
|
|
206
244
|
}
|
|
207
245
|
|
|
208
|
-
async stop() {
|
|
246
|
+
async stop(): Promise<void> {
|
|
209
247
|
if (this.sound) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
248
|
+
try {
|
|
249
|
+
await this.sound.stopAsync();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to stop sound';
|
|
252
|
+
if (__DEV__) console.error('[AudioManager] Error stopping sound:', error);
|
|
253
|
+
useSoundStore.getState().setError(errorMessage);
|
|
254
|
+
useSoundStore.getState().setPlaying(false);
|
|
255
|
+
useSoundStore.getState().setProgress(0, 0);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
213
258
|
}
|
|
214
259
|
}
|
|
215
260
|
|
|
216
|
-
async seek(positionMillis: number) {
|
|
261
|
+
async seek(positionMillis: number): Promise<void> {
|
|
262
|
+
if (!Number.isFinite(positionMillis)) {
|
|
263
|
+
throw new Error('Invalid position: positionMillis must be a finite number');
|
|
264
|
+
}
|
|
265
|
+
|
|
217
266
|
if (this.sound) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
267
|
+
try {
|
|
268
|
+
const status = await this.sound.getStatusAsync();
|
|
269
|
+
if (status.isLoaded) {
|
|
270
|
+
const clampedPosition = Math.max(0, Math.min(status.durationMillis || 0, positionMillis));
|
|
271
|
+
await this.sound.setStatusAsync({ positionMillis: clampedPosition });
|
|
272
|
+
} else {
|
|
273
|
+
throw new Error('Cannot seek: sound is not loaded');
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to seek sound';
|
|
277
|
+
if (__DEV__) console.error('[AudioManager] Error seeking sound:', error);
|
|
278
|
+
useSoundStore.getState().setError(errorMessage);
|
|
279
|
+
throw error;
|
|
221
280
|
}
|
|
222
281
|
}
|
|
223
282
|
}
|
|
224
283
|
|
|
225
|
-
async setVolume(volume: number) {
|
|
284
|
+
async setVolume(volume: number): Promise<void> {
|
|
226
285
|
if (this.sound) {
|
|
227
286
|
const clampedVolume = clampVolume(volume);
|
|
228
287
|
await this.sound.setVolumeAsync(clampedVolume);
|
|
@@ -230,21 +289,25 @@ class AudioManager {
|
|
|
230
289
|
}
|
|
231
290
|
}
|
|
232
291
|
|
|
233
|
-
async setRate(rate: number) {
|
|
292
|
+
async setRate(rate: number): Promise<void> {
|
|
234
293
|
if (this.sound) {
|
|
235
294
|
const clampedRate = clampRate(rate);
|
|
236
295
|
const status = await this.sound.getStatusAsync();
|
|
237
296
|
if (status.isLoaded) {
|
|
238
297
|
try {
|
|
239
298
|
await this.sound.setRateAsync(clampedRate, false);
|
|
299
|
+
useSoundStore.getState().setRateState(clampedRate);
|
|
240
300
|
} catch (error) {
|
|
301
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to set rate';
|
|
241
302
|
if (__DEV__) console.warn('[AudioManager] Could not set rate:', error);
|
|
303
|
+
useSoundStore.getState().setError(errorMessage);
|
|
304
|
+
throw error;
|
|
242
305
|
}
|
|
243
306
|
}
|
|
244
307
|
}
|
|
245
308
|
}
|
|
246
309
|
|
|
247
|
-
async unload() {
|
|
310
|
+
async unload(): Promise<void> {
|
|
248
311
|
if (this.sound) {
|
|
249
312
|
try {
|
|
250
313
|
await this.sound.unloadAsync();
|
|
@@ -257,18 +320,16 @@ class AudioManager {
|
|
|
257
320
|
useSoundStore.getState().reset();
|
|
258
321
|
}
|
|
259
322
|
|
|
260
|
-
clearCache() {
|
|
323
|
+
clearCache(): void {
|
|
261
324
|
for (const cached of this.cache.values()) {
|
|
262
|
-
cached.sound.unloadAsync().catch(() => {
|
|
325
|
+
cached.sound.unloadAsync().catch((error) => {
|
|
326
|
+
if (__DEV__) console.warn('[AudioManager] Failed to unload sound during cache clear:', error);
|
|
327
|
+
});
|
|
263
328
|
}
|
|
264
329
|
this.cache.clear();
|
|
265
330
|
if (__DEV__) console.log('[AudioManager] Cache cleared');
|
|
266
331
|
}
|
|
267
332
|
|
|
268
|
-
getCurrentId() {
|
|
269
|
-
return this.currentId;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
333
|
isCached(id: string): boolean {
|
|
273
334
|
return this.cache.has(id);
|
|
274
335
|
}
|
package/src/global.d.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export type { SoundSource, PlaybackOptions, SoundState } from './types';
|
|
2
|
+
export {
|
|
3
|
+
isPlaybackStatusSuccess,
|
|
4
|
+
isSoundSourceValid,
|
|
5
|
+
clampVolume,
|
|
6
|
+
clampRate,
|
|
7
|
+
validateSoundId,
|
|
8
|
+
} from './utils';
|
|
8
9
|
export { audioManager } from './AudioManager';
|
|
10
|
+
export { useSound } from './useSound';
|
|
9
11
|
export { useSoundStore } from './store';
|
package/src/store.ts
CHANGED
|
@@ -8,6 +8,8 @@ interface SoundActions {
|
|
|
8
8
|
setError: (error: string | null) => void;
|
|
9
9
|
setCurrent: (id: string | null, source: SoundSource | null) => void;
|
|
10
10
|
setVolumeState: (volume: number) => void;
|
|
11
|
+
setRateState: (rate: number) => void;
|
|
12
|
+
setLooping: (isLooping: boolean) => void;
|
|
11
13
|
reset: () => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -18,6 +20,7 @@ const initialSoundState: SoundState = {
|
|
|
18
20
|
durationMillis: 0,
|
|
19
21
|
volume: 1.0,
|
|
20
22
|
rate: 1.0,
|
|
23
|
+
isLooping: false,
|
|
21
24
|
error: null,
|
|
22
25
|
currentSource: null,
|
|
23
26
|
currentId: null,
|
|
@@ -35,6 +38,8 @@ export const useSoundStore = createStore<SoundState, SoundActions>({
|
|
|
35
38
|
setError: (error) => set({ error }),
|
|
36
39
|
setCurrent: (id, source) => set({ currentId: id, currentSource: source }),
|
|
37
40
|
setVolumeState: (volume) => set({ volume }),
|
|
41
|
+
setRateState: (rate) => set({ rate }),
|
|
42
|
+
setLooping: (isLooping) => set({ isLooping }),
|
|
38
43
|
reset: () => set(initialSoundState),
|
|
39
44
|
}),
|
|
40
45
|
});
|
package/src/types.ts
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* @umituz/react-native-sound Types
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { AVPlaybackStatus
|
|
5
|
+
import { AVPlaybackStatus } from 'expo-av';
|
|
6
6
|
|
|
7
7
|
export type SoundSource = number | { uri: string; headers?: Record<string, string> } | null;
|
|
8
8
|
|
|
9
9
|
export interface PlaybackOptions {
|
|
10
|
-
shouldPlay?: boolean;
|
|
11
10
|
isLooping?: boolean;
|
|
12
11
|
volume?: number;
|
|
13
12
|
rate?: number;
|
|
@@ -21,41 +20,8 @@ export interface SoundState {
|
|
|
21
20
|
durationMillis: number;
|
|
22
21
|
volume: number;
|
|
23
22
|
rate: number;
|
|
23
|
+
isLooping: boolean;
|
|
24
24
|
error: string | null;
|
|
25
25
|
currentSource: SoundSource | null;
|
|
26
26
|
currentId: string | null;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
export type PlaybackStatus = AVPlaybackStatus;
|
|
30
|
-
|
|
31
|
-
export interface SoundError {
|
|
32
|
-
message: string;
|
|
33
|
-
code?: string;
|
|
34
|
-
originalError?: unknown;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type PlaybackStatusSuccess = AVPlaybackStatusSuccess;
|
|
38
|
-
|
|
39
|
-
export function isPlaybackStatusSuccess(status: AVPlaybackStatus): status is PlaybackStatusSuccess {
|
|
40
|
-
return status.isLoaded;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function isSoundSourceValid(source: SoundSource): source is number | { uri: string; headers?: Record<string, string> } {
|
|
44
|
-
return source !== null && source !== undefined;
|
|
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,36 +1,45 @@
|
|
|
1
|
-
import { useCallback
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
2
|
import { useSoundStore } from './store';
|
|
3
3
|
import { audioManager } from './AudioManager';
|
|
4
4
|
import { PlaybackOptions, SoundSource } from './types';
|
|
5
5
|
|
|
6
|
-
export const useSound = ()
|
|
6
|
+
export const useSound = (): {
|
|
7
|
+
play: (id: string, source: SoundSource, options?: PlaybackOptions) => Promise<void>;
|
|
8
|
+
pause: () => Promise<void>;
|
|
9
|
+
resume: () => Promise<void>;
|
|
10
|
+
stop: () => Promise<void>;
|
|
11
|
+
seek: (positionMillis: number) => Promise<void>;
|
|
12
|
+
setVolume: (vol: number) => Promise<void>;
|
|
13
|
+
setRate: (rate: number) => Promise<void>;
|
|
14
|
+
preload: (id: string, source: SoundSource, options?: PlaybackOptions) => Promise<void>;
|
|
15
|
+
unload: () => Promise<void>;
|
|
16
|
+
clearCache: () => void;
|
|
17
|
+
isCached: (id: string) => boolean;
|
|
18
|
+
isPlaying: boolean;
|
|
19
|
+
isBuffering: boolean;
|
|
20
|
+
isLooping: boolean;
|
|
21
|
+
position: number;
|
|
22
|
+
duration: number;
|
|
23
|
+
currentId: string | null;
|
|
24
|
+
error: string | null;
|
|
25
|
+
volume: number;
|
|
26
|
+
rate: number;
|
|
27
|
+
} => {
|
|
7
28
|
const isPlaying = useSoundStore((state) => state.isPlaying);
|
|
8
29
|
const isBuffering = useSoundStore((state) => state.isBuffering);
|
|
30
|
+
const isLooping = useSoundStore((state) => state.isLooping);
|
|
9
31
|
const position = useSoundStore((state) => state.positionMillis);
|
|
10
32
|
const duration = useSoundStore((state) => state.durationMillis);
|
|
11
33
|
const currentId = useSoundStore((state) => state.currentId);
|
|
12
34
|
const error = useSoundStore((state) => state.error);
|
|
13
35
|
const volume = useSoundStore((state) => state.volume);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
return () => {
|
|
17
|
-
if (__DEV__) console.log('[useSound] Cleanup on unmount');
|
|
18
|
-
};
|
|
19
|
-
}, []);
|
|
36
|
+
const rate = useSoundStore((state) => state.rate);
|
|
20
37
|
|
|
21
38
|
const play = useCallback(
|
|
22
39
|
async (id: string, source: SoundSource, options?: PlaybackOptions) => {
|
|
23
|
-
|
|
24
|
-
if (isPlaying) {
|
|
25
|
-
await audioManager.pause();
|
|
26
|
-
} else {
|
|
27
|
-
await audioManager.resume();
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
await audioManager.play(id, source, options);
|
|
31
|
-
}
|
|
40
|
+
await audioManager.play(id, source, options);
|
|
32
41
|
},
|
|
33
|
-
[
|
|
42
|
+
[]
|
|
34
43
|
);
|
|
35
44
|
|
|
36
45
|
const pause = useCallback(async () => {
|
|
@@ -87,10 +96,12 @@ export const useSound = () => {
|
|
|
87
96
|
isCached,
|
|
88
97
|
isPlaying,
|
|
89
98
|
isBuffering,
|
|
99
|
+
isLooping,
|
|
90
100
|
position,
|
|
91
101
|
duration,
|
|
92
102
|
currentId,
|
|
93
103
|
error,
|
|
94
104
|
volume,
|
|
105
|
+
rate,
|
|
95
106
|
};
|
|
96
107
|
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-sound Utility Functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AVPlaybackStatus, AVPlaybackStatusSuccess } from 'expo-av';
|
|
6
|
+
import type { SoundSource } from './types';
|
|
7
|
+
|
|
8
|
+
type PlaybackStatusSuccess = AVPlaybackStatusSuccess;
|
|
9
|
+
|
|
10
|
+
export function isPlaybackStatusSuccess(status: AVPlaybackStatus): status is PlaybackStatusSuccess {
|
|
11
|
+
return status.isLoaded;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isSoundSourceValid(source: SoundSource): source is number | { uri: string; headers?: Record<string, string> } {
|
|
15
|
+
return source !== null && source !== undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function clampVolume(volume: number): number {
|
|
19
|
+
if (!Number.isFinite(volume)) return 1.0;
|
|
20
|
+
return Math.max(0, Math.min(1, volume));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clampRate(rate: number): number {
|
|
24
|
+
if (!Number.isFinite(rate)) return 1.0;
|
|
25
|
+
return Math.max(0.5, Math.min(2.0, rate));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateSoundId(id: string): boolean {
|
|
29
|
+
return typeof id === 'string' && id.trim().length > 0;
|
|
30
|
+
}
|