@umituz/react-native-sound 1.2.30 → 1.2.32

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.
Files changed (33) hide show
  1. package/package.json +1 -5
  2. package/src/application/SoundServiceFacade.ts +112 -0
  3. package/src/application/interfaces/IAudioService.ts +42 -0
  4. package/src/application/interfaces/ISoundCache.ts +21 -0
  5. package/src/application/presenters/SoundPresenter.ts +78 -0
  6. package/src/application/use-cases/BaseUseCase.ts +36 -0
  7. package/src/application/use-cases/ClearCacheUseCase.ts +16 -0
  8. package/src/application/use-cases/PauseSoundUseCase.ts +26 -0
  9. package/src/application/use-cases/PlaySoundUseCase.ts +106 -0
  10. package/src/application/use-cases/PreloadSoundUseCase.ts +56 -0
  11. package/src/application/use-cases/ResumeSoundUseCase.ts +25 -0
  12. package/src/application/use-cases/SeekSoundUseCase.ts +26 -0
  13. package/src/application/use-cases/SetRateUseCase.ts +30 -0
  14. package/src/application/use-cases/SetVolumeUseCase.ts +30 -0
  15. package/src/application/use-cases/StopSoundUseCase.ts +27 -0
  16. package/src/domain/entities/SoundState.ts +83 -0
  17. package/src/domain/errors/SoundError.ts +44 -0
  18. package/src/domain/value-objects/PlaybackPosition.ts +30 -0
  19. package/src/domain/value-objects/Rate.ts +26 -0
  20. package/src/domain/value-objects/SoundId.ts +22 -0
  21. package/src/domain/value-objects/SoundSource.ts +30 -0
  22. package/src/domain/value-objects/Volume.ts +26 -0
  23. package/src/index.ts +36 -6
  24. package/src/infrastructure/AudioConfig.ts +25 -0
  25. package/src/infrastructure/ExpoAudioService.ts +88 -0
  26. package/src/infrastructure/Logger.ts +23 -0
  27. package/src/infrastructure/SoundCache.ts +70 -0
  28. package/src/presentation/hooks/useSound.ts +47 -0
  29. package/src/{store.ts → presentation/store.ts} +22 -9
  30. package/src/types.ts +6 -5
  31. package/src/utils.ts +13 -17
  32. package/src/AudioManager.ts +0 -338
  33. package/src/useSound.ts +0 -107
package/src/utils.ts CHANGED
@@ -1,30 +1,26 @@
1
1
  /**
2
- * @umituz/react-native-sound Utility Functions
2
+ * Utility Functions - Backward Compatibility
3
3
  */
4
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
- }
5
+ import { Volume } from './domain/value-objects/Volume';
6
+ import { Rate } from './domain/value-objects/Rate';
17
7
 
18
8
  export function clampVolume(volume: number): number {
19
- if (!Number.isFinite(volume)) return 1.0;
20
- return Math.max(0, Math.min(1, volume));
9
+ return new Volume(volume).getValue();
21
10
  }
22
11
 
23
12
  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));
13
+ return new Rate(rate).getValue();
26
14
  }
27
15
 
28
16
  export function validateSoundId(id: string): boolean {
29
17
  return typeof id === 'string' && id.trim().length > 0;
30
18
  }
19
+
20
+ export function isSoundSourceValid(source: any): boolean {
21
+ return source !== null && source !== undefined;
22
+ }
23
+
24
+ export function isPlaybackStatusSuccess(status: any): boolean {
25
+ return status.isLoaded === true;
26
+ }
@@ -1,338 +0,0 @@
1
- import { Audio, AVPlaybackStatus, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
2
- import { useSoundStore } from './store';
3
- import { PlaybackOptions, SoundSource } from './types';
4
- import {
5
- isPlaybackStatusSuccess,
6
- isSoundSourceValid,
7
- clampVolume,
8
- clampRate,
9
- validateSoundId,
10
- } from './utils';
11
-
12
- interface CachedSound {
13
- sound: Audio.Sound;
14
- source: SoundSource;
15
- loadedAt: number;
16
- }
17
-
18
- class AudioManager {
19
- private sound: Audio.Sound | null = null;
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;
24
-
25
- constructor() {
26
- this.configureAudio().catch((error) => {
27
- if (__DEV__) console.error('[AudioManager] Failed to configure audio on initialization:', error);
28
- });
29
- }
30
-
31
- private async configureAudio() {
32
- try {
33
- await Audio.setAudioModeAsync({
34
- playsInSilentModeIOS: true,
35
- staysActiveInBackground: false,
36
- shouldDuckAndroid: true,
37
- playThroughEarpieceAndroid: false,
38
- interruptionModeIOS: InterruptionModeIOS.DuckOthers,
39
- interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
40
- });
41
- } catch (error) {
42
- if (__DEV__) console.warn('Failed to configure audio session', error);
43
- }
44
- }
45
-
46
- private onPlaybackStatusUpdate = (status: AVPlaybackStatus) => {
47
- const store = useSoundStore.getState();
48
-
49
- if (!isPlaybackStatusSuccess(status)) {
50
- if (status.error) {
51
- store.setError(status.error);
52
- store.setPlaying(false);
53
- }
54
- return;
55
- }
56
-
57
- store.setPlaying(status.isPlaying);
58
- store.setBuffering(status.isBuffering);
59
- store.setProgress(status.positionMillis, status.durationMillis || 0);
60
-
61
- if (status.isLoaded) {
62
- store.setRateState(status.rate || 1.0);
63
- }
64
-
65
- if (status.didJustFinish && !status.isLooping) {
66
- store.setPlaying(false);
67
- store.setProgress(status.durationMillis || 0, status.durationMillis || 0);
68
- }
69
- };
70
-
71
- private cleanExpiredCache() {
72
- const now = Date.now();
73
- for (const [id, cached] of this.cache) {
74
- if (now - cached.loadedAt > this.cacheExpireMs) {
75
- cached.sound.unloadAsync().catch((error) => {
76
- if (__DEV__) console.warn('[AudioManager] Failed to unload expired cache:', id, error);
77
- });
78
- this.cache.delete(id);
79
- if (__DEV__) console.log('[AudioManager] Expired cache removed:', id);
80
- }
81
- }
82
- }
83
-
84
- private enforceCacheLimit() {
85
- if (this.cache.size >= this.maxCacheSize) {
86
- const firstKey = this.cache.keys().next().value;
87
- if (firstKey) {
88
- const cached = this.cache.get(firstKey);
89
- cached?.sound.unloadAsync().catch((error) => {
90
- if (__DEV__) console.warn('[AudioManager] Failed to unload sound during cache limit enforcement:', firstKey, error);
91
- });
92
- this.cache.delete(firstKey);
93
- if (__DEV__) console.log('[AudioManager] Cache limit enforced, removed:', firstKey);
94
- }
95
- }
96
- }
97
-
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
-
103
- if (!isSoundSourceValid(source)) {
104
- throw new Error('Invalid sound source: source is null or undefined');
105
- }
106
-
107
- if (this.cache.has(id)) {
108
- if (__DEV__) console.log('[AudioManager] Already cached:', id);
109
- return;
110
- }
111
-
112
- try {
113
- this.cleanExpiredCache();
114
- this.enforceCacheLimit();
115
-
116
- const volume = options?.volume !== undefined ? clampVolume(options.volume) : 1.0;
117
- const rate = options?.rate !== undefined ? clampRate(options.rate) : 1.0;
118
-
119
- const { sound } = await Audio.Sound.createAsync(
120
- source,
121
- {
122
- shouldPlay: false,
123
- isLooping: options?.isLooping ?? false,
124
- volume,
125
- rate,
126
- },
127
- this.onPlaybackStatusUpdate
128
- );
129
-
130
- this.cache.set(id, { sound, source, loadedAt: Date.now() });
131
- if (__DEV__) console.log('[AudioManager] Preloaded:', id);
132
- } catch (error) {
133
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
134
- if (__DEV__) console.error('[AudioManager] Error preloading sound:', error);
135
- throw new Error(errorMessage);
136
- }
137
- }
138
-
139
- async play(id: string, source: SoundSource, options?: PlaybackOptions): Promise<void> {
140
- const store = useSoundStore.getState();
141
-
142
- if (__DEV__) console.log('[AudioManager] Play called with ID:', id);
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
-
150
- if (!isSoundSourceValid(source)) {
151
- const errorMsg = 'Invalid sound source: source is null or undefined';
152
- store.setError(errorMsg);
153
- throw new Error(errorMsg);
154
- }
155
-
156
- if (this.currentId === id && this.sound) {
157
- const status = await this.sound.getStatusAsync();
158
- if (status.isLoaded) {
159
- if (status.isPlaying) {
160
- store.setPlaying(false);
161
- await this.sound.pauseAsync();
162
- return;
163
- } else {
164
- if (__DEV__) console.log('[AudioManager] Resuming existing sound');
165
- store.setPlaying(true);
166
- await this.sound.playAsync();
167
- return;
168
- }
169
- }
170
- }
171
-
172
- try {
173
- await this.unload();
174
-
175
- const volume = options?.volume !== undefined ? clampVolume(options.volume) : 1.0;
176
- const rate = options?.rate !== undefined ? clampRate(options.rate) : 1.0;
177
- const isLooping = options?.isLooping ?? false;
178
-
179
- const cached = this.cache.get(id);
180
- if (cached) {
181
- this.sound = cached.sound;
182
- this.cache.delete(id);
183
- if (__DEV__) console.log('[AudioManager] Using cached sound:', id);
184
- } else {
185
- const { sound } = await Audio.Sound.createAsync(
186
- source,
187
- {
188
- shouldPlay: true,
189
- isLooping,
190
- volume,
191
- rate,
192
- positionMillis: options?.positionMillis ?? 0,
193
- },
194
- this.onPlaybackStatusUpdate
195
- );
196
- this.sound = sound;
197
- if (__DEV__) console.log('[AudioManager] Sound created and playing');
198
- }
199
-
200
- this.currentId = id;
201
- store.setCurrent(id, source);
202
- store.setError(null);
203
- store.setLooping(isLooping);
204
- store.setRateState(rate);
205
- store.setVolumeState(volume);
206
-
207
- await this.sound?.playAsync();
208
- } catch (error) {
209
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
210
- if (__DEV__) console.error('[AudioManager] Error playing sound:', error);
211
- store.setError(errorMessage);
212
- this.currentId = null;
213
- this.sound = null;
214
- throw error;
215
- }
216
- }
217
-
218
- async pause(): Promise<void> {
219
- if (this.sound) {
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
- }
229
- }
230
- }
231
-
232
- async resume(): Promise<void> {
233
- if (this.sound) {
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
- }
243
- }
244
- }
245
-
246
- async stop(): Promise<void> {
247
- if (this.sound) {
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
- }
258
- }
259
- }
260
-
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
-
266
- if (this.sound) {
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;
280
- }
281
- }
282
- }
283
-
284
- async setVolume(volume: number): Promise<void> {
285
- if (this.sound) {
286
- const clampedVolume = clampVolume(volume);
287
- await this.sound.setVolumeAsync(clampedVolume);
288
- useSoundStore.getState().setVolumeState(clampedVolume);
289
- }
290
- }
291
-
292
- async setRate(rate: number): Promise<void> {
293
- if (this.sound) {
294
- const clampedRate = clampRate(rate);
295
- const status = await this.sound.getStatusAsync();
296
- if (status.isLoaded) {
297
- try {
298
- await this.sound.setRateAsync(clampedRate, false);
299
- useSoundStore.getState().setRateState(clampedRate);
300
- } catch (error) {
301
- const errorMessage = error instanceof Error ? error.message : 'Failed to set rate';
302
- if (__DEV__) console.warn('[AudioManager] Could not set rate:', error);
303
- useSoundStore.getState().setError(errorMessage);
304
- throw error;
305
- }
306
- }
307
- }
308
- }
309
-
310
- async unload(): Promise<void> {
311
- if (this.sound) {
312
- try {
313
- await this.sound.unloadAsync();
314
- } catch (e) {
315
- if (__DEV__) console.warn('[AudioManager] Error unloading sound:', e);
316
- }
317
- this.sound = null;
318
- }
319
- this.currentId = null;
320
- useSoundStore.getState().reset();
321
- }
322
-
323
- clearCache(): void {
324
- for (const cached of this.cache.values()) {
325
- cached.sound.unloadAsync().catch((error) => {
326
- if (__DEV__) console.warn('[AudioManager] Failed to unload sound during cache clear:', error);
327
- });
328
- }
329
- this.cache.clear();
330
- if (__DEV__) console.log('[AudioManager] Cache cleared');
331
- }
332
-
333
- isCached(id: string): boolean {
334
- return this.cache.has(id);
335
- }
336
- }
337
-
338
- export const audioManager = new AudioManager();
package/src/useSound.ts DELETED
@@ -1,107 +0,0 @@
1
- import { useCallback } from 'react';
2
- import { useSoundStore } from './store';
3
- import { audioManager } from './AudioManager';
4
- import { PlaybackOptions, SoundSource } from './types';
5
-
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
- } => {
28
- const isPlaying = useSoundStore((state) => state.isPlaying);
29
- const isBuffering = useSoundStore((state) => state.isBuffering);
30
- const isLooping = useSoundStore((state) => state.isLooping);
31
- const position = useSoundStore((state) => state.positionMillis);
32
- const duration = useSoundStore((state) => state.durationMillis);
33
- const currentId = useSoundStore((state) => state.currentId);
34
- const error = useSoundStore((state) => state.error);
35
- const volume = useSoundStore((state) => state.volume);
36
- const rate = useSoundStore((state) => state.rate);
37
-
38
- const play = useCallback(
39
- async (id: string, source: SoundSource, options?: PlaybackOptions) => {
40
- await audioManager.play(id, source, options);
41
- },
42
- []
43
- );
44
-
45
- const pause = useCallback(async () => {
46
- await audioManager.pause();
47
- }, []);
48
-
49
- const resume = useCallback(async () => {
50
- await audioManager.resume();
51
- }, []);
52
-
53
- const stop = useCallback(async () => {
54
- await audioManager.stop();
55
- }, []);
56
-
57
- const seek = useCallback(async (positionMillis: number) => {
58
- await audioManager.seek(positionMillis);
59
- }, []);
60
-
61
- const setVolume = useCallback(async (vol: number) => {
62
- await audioManager.setVolume(vol);
63
- }, []);
64
-
65
- const setRate = useCallback(async (rate: number) => {
66
- await audioManager.setRate(rate);
67
- }, []);
68
-
69
- const preload = useCallback(async (id: string, source: SoundSource, options?: PlaybackOptions) => {
70
- await audioManager.preload(id, source, options);
71
- }, []);
72
-
73
- const unload = useCallback(async () => {
74
- await audioManager.unload();
75
- }, []);
76
-
77
- const clearCache = useCallback(() => {
78
- audioManager.clearCache();
79
- }, []);
80
-
81
- const isCached = useCallback((id: string): boolean => {
82
- return audioManager.isCached(id);
83
- }, []);
84
-
85
- return {
86
- play,
87
- pause,
88
- resume,
89
- stop,
90
- seek,
91
- setVolume,
92
- setRate,
93
- preload,
94
- unload,
95
- clearCache,
96
- isCached,
97
- isPlaying,
98
- isBuffering,
99
- isLooping,
100
- position,
101
- duration,
102
- currentId,
103
- error,
104
- volume,
105
- rate,
106
- };
107
- };