@umituz/react-native-sound 1.2.19 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-sound",
3
- "version": "1.2.19",
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
- "@umituz/react-native-design-system": "latest",
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
- "react": "19.1.0",
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": {
@@ -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
- } from './types';
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: options?.isLooping ?? false,
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
- 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
- }
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
- await this.sound.pauseAsync();
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
- await this.sound.playAsync();
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
- await this.sound.stopAsync();
211
- useSoundStore.getState().setPlaying(false);
212
- useSoundStore.getState().setProgress(0, 0);
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
- const status = await this.sound.getStatusAsync();
219
- if (status.isLoaded) {
220
- await this.sound.setStatusAsync({ positionMillis: Math.max(0, positionMillis) });
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
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Global type declarations for React Native
3
+ */
4
+
5
+ declare const __DEV__: boolean;
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
- /**
2
- * @umituz/react-native-sound
3
- * Universal sound playback library with caching
4
- */
5
-
6
- export * from './types';
7
- export * from './useSound';
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, AVPlaybackStatusSuccess } from 'expo-av';
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, useEffect } from 'react';
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
- if (currentId === id) {
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
- [currentId, isPlaying]
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
+ }