@umituz/react-native-sound 1.2.31 → 1.2.33
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/application/SoundCommands.ts +233 -0
- package/src/application/SoundEvents.ts +101 -0
- package/src/application/SoundService.ts +185 -0
- package/src/application/interfaces/IAudioService.ts +42 -0
- package/src/application/interfaces/ISoundCache.ts +19 -0
- package/src/domain/errors/SoundError.ts +44 -0
- package/src/domain/value-objects.ts +128 -0
- package/src/index.ts +45 -6
- package/src/infrastructure/AudioConfig.ts +24 -0
- package/src/infrastructure/AudioRepository.ts +216 -0
- package/src/infrastructure/Logger.ts +23 -0
- package/src/presentation/SoundStore.ts +156 -0
- package/src/presentation/useSound.ts +183 -0
- package/src/types.ts +5 -5
- package/src/utils.ts +173 -15
- package/src/AudioManager.ts +0 -338
- package/src/store.ts +0 -45
- package/src/useSound.ts +0 -107
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSound Hook - Event-Driven with Proper Lifecycle Management
|
|
3
|
+
* Single hook for all sound functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useCallback, useRef } from 'react';
|
|
7
|
+
import { useSoundStore, setupEventListeners } from './SoundStore';
|
|
8
|
+
import { SoundService } from '../application/SoundService';
|
|
9
|
+
import type { SoundSourceValue } from '../domain/value-objects';
|
|
10
|
+
import type { PlaybackOptions } from '../types';
|
|
11
|
+
import { Logger } from '../infrastructure/Logger';
|
|
12
|
+
|
|
13
|
+
let serviceInstance: SoundService | null = null;
|
|
14
|
+
let serviceRefCount = 0;
|
|
15
|
+
let cleanupEventListeners: (() => void) | null = null;
|
|
16
|
+
|
|
17
|
+
const getService = (): SoundService => {
|
|
18
|
+
if (!serviceInstance) {
|
|
19
|
+
serviceInstance = SoundService.getInstance();
|
|
20
|
+
// Access events through public API
|
|
21
|
+
cleanupEventListeners = setupEventListeners(serviceInstance.getEvents());
|
|
22
|
+
}
|
|
23
|
+
serviceRefCount++;
|
|
24
|
+
return serviceInstance;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const releaseService = async (): Promise<void> => {
|
|
28
|
+
serviceRefCount--;
|
|
29
|
+
if (serviceRefCount <= 0 && serviceInstance) {
|
|
30
|
+
try {
|
|
31
|
+
await serviceInstance.dispose();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
Logger.warn('Error during service cleanup', error);
|
|
34
|
+
}
|
|
35
|
+
cleanupEventListeners?.();
|
|
36
|
+
cleanupEventListeners = null;
|
|
37
|
+
serviceInstance = null;
|
|
38
|
+
serviceRefCount = 0;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const useSound = () => {
|
|
43
|
+
const state = useSoundStore();
|
|
44
|
+
const serviceRef = useRef<SoundService | null>(null);
|
|
45
|
+
const isMountedRef = useRef(true);
|
|
46
|
+
|
|
47
|
+
// Initialize service on mount
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
serviceRef.current = getService();
|
|
50
|
+
isMountedRef.current = true;
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
isMountedRef.current = false;
|
|
54
|
+
// Don't await in cleanup - fire and forget
|
|
55
|
+
releaseService().catch((error) => Logger.warn('Error in useSound cleanup', error));
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
// Memoize all functions to prevent re-renders
|
|
60
|
+
const play = useCallback(
|
|
61
|
+
(id: string, source: SoundSourceValue, options?: PlaybackOptions) => {
|
|
62
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
63
|
+
return serviceRef.current.play(id, source, options);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const pause = useCallback(() => {
|
|
70
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
71
|
+
return serviceRef.current.pause();
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const resume = useCallback(() => {
|
|
76
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
77
|
+
return serviceRef.current.resume();
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const stop = useCallback(() => {
|
|
82
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
83
|
+
return serviceRef.current.stop();
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const seek = useCallback(
|
|
88
|
+
(positionMillis: number) => {
|
|
89
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
90
|
+
return serviceRef.current.seek(positionMillis);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const setVolume = useCallback(
|
|
97
|
+
(volume: number) => {
|
|
98
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
99
|
+
return serviceRef.current.setVolume(volume);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const setRate = useCallback(
|
|
106
|
+
(rate: number) => {
|
|
107
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
108
|
+
return serviceRef.current.setRate(rate);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const preload = useCallback(
|
|
115
|
+
(id: string, source: SoundSourceValue, options?: PlaybackOptions) => {
|
|
116
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
117
|
+
return serviceRef.current.preload(id, source, options);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const unload = useCallback(() => {
|
|
124
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
125
|
+
return serviceRef.current.unload();
|
|
126
|
+
}
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const clearCache = useCallback(async () => {
|
|
130
|
+
if (isMountedRef.current && serviceRef.current) {
|
|
131
|
+
await serviceRef.current.clearCache();
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const isCached = useCallback(
|
|
136
|
+
(id: string) => {
|
|
137
|
+
if (serviceRef.current) {
|
|
138
|
+
return serviceRef.current.isCached(id);
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
},
|
|
142
|
+
[]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const subscribe = useCallback(
|
|
146
|
+
(eventType: string, listener: (payload: unknown) => void) => {
|
|
147
|
+
if (serviceRef.current) {
|
|
148
|
+
return serviceRef.current.on(eventType, listener);
|
|
149
|
+
}
|
|
150
|
+
return () => {};
|
|
151
|
+
},
|
|
152
|
+
[]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
// State
|
|
157
|
+
isPlaying: state.isPlaying,
|
|
158
|
+
isBuffering: state.isBuffering,
|
|
159
|
+
isLooping: state.isLooping,
|
|
160
|
+
position: state.positionMillis,
|
|
161
|
+
duration: state.durationMillis,
|
|
162
|
+
currentId: state.currentId,
|
|
163
|
+
error: state.error,
|
|
164
|
+
volume: state.volume,
|
|
165
|
+
rate: state.rate,
|
|
166
|
+
|
|
167
|
+
// Actions (memoized)
|
|
168
|
+
play,
|
|
169
|
+
pause,
|
|
170
|
+
resume,
|
|
171
|
+
stop,
|
|
172
|
+
seek,
|
|
173
|
+
setVolume,
|
|
174
|
+
setRate,
|
|
175
|
+
preload,
|
|
176
|
+
unload,
|
|
177
|
+
clearCache,
|
|
178
|
+
isCached,
|
|
179
|
+
|
|
180
|
+
// Advanced: Event subscription
|
|
181
|
+
on: subscribe,
|
|
182
|
+
};
|
|
183
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Public Types
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export type SoundSource = number | { uri: string; headers?: Record<string, string> } | null;
|
|
5
|
+
export type { SoundSourceValue } from './domain/value-objects';
|
|
8
6
|
|
|
9
7
|
export interface PlaybackOptions {
|
|
10
8
|
isLooping?: boolean;
|
|
@@ -22,6 +20,8 @@ export interface SoundState {
|
|
|
22
20
|
rate: number;
|
|
23
21
|
isLooping: boolean;
|
|
24
22
|
error: string | null;
|
|
25
|
-
currentSource:
|
|
23
|
+
currentSource: any;
|
|
26
24
|
currentId: string | null;
|
|
27
25
|
}
|
|
26
|
+
|
|
27
|
+
export type SoundSource = number | { uri: string; headers?: Record<string, string> } | null;
|
package/src/utils.ts
CHANGED
|
@@ -1,30 +1,188 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Utility Functions - Backward Compatibility + Performance Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { Volume } from './domain/value-objects';
|
|
6
|
+
import { Rate } from './domain/value-objects';
|
|
7
|
+
import type { PlaybackStatus } from './application/interfaces/IAudioService';
|
|
8
|
+
import { Logger } from './infrastructure/Logger';
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
// ===== Backward Compatibility Functions =====
|
|
9
11
|
|
|
10
|
-
export function
|
|
11
|
-
return
|
|
12
|
+
export function clampVolume(volume: number): number {
|
|
13
|
+
return new Volume(volume).getValue();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function clampRate(rate: number): number {
|
|
17
|
+
return new Rate(rate).getValue();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateSoundId(id: string): boolean {
|
|
21
|
+
return typeof id === 'string' && id.trim().length > 0;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
|
-
export function isSoundSourceValid(source:
|
|
24
|
+
export function isSoundSourceValid(source: unknown): boolean {
|
|
15
25
|
return source !== null && source !== undefined;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
return Math.max(0, Math.min(1, volume));
|
|
28
|
+
export function isPlaybackStatusSuccess(status: PlaybackStatus): boolean {
|
|
29
|
+
return status.isLoaded === true;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
// ===== Performance Utilities =====
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Debounce function to limit execution frequency
|
|
36
|
+
* @param func Function to debounce
|
|
37
|
+
* @param wait Wait time in milliseconds
|
|
38
|
+
* @returns Debounced function
|
|
39
|
+
*/
|
|
40
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
41
|
+
func: T,
|
|
42
|
+
wait: number
|
|
43
|
+
): (...args: Parameters<T>) => void {
|
|
44
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
45
|
+
|
|
46
|
+
return function (this: unknown, ...args: Parameters<T>) {
|
|
47
|
+
if (timeoutId) {
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
timeoutId = setTimeout(() => {
|
|
52
|
+
func.apply(this, args);
|
|
53
|
+
timeoutId = null;
|
|
54
|
+
}, wait);
|
|
55
|
+
};
|
|
26
56
|
}
|
|
27
57
|
|
|
28
|
-
|
|
29
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Throttle function to limit execution frequency
|
|
60
|
+
* @param func Function to throttle
|
|
61
|
+
* @param limit Time limit in milliseconds
|
|
62
|
+
* @returns Throttled function
|
|
63
|
+
*/
|
|
64
|
+
export function throttle<T extends (...args: unknown[]) => unknown>(
|
|
65
|
+
func: T,
|
|
66
|
+
limit: number
|
|
67
|
+
): (...args: Parameters<T>) => void {
|
|
68
|
+
let inThrottle = false;
|
|
69
|
+
let lastArgs: Parameters<T> | null = null;
|
|
70
|
+
let lastContext: unknown = null;
|
|
71
|
+
|
|
72
|
+
return function (this: unknown, ...args: Parameters<T>) {
|
|
73
|
+
if (!inThrottle) {
|
|
74
|
+
func.apply(this, args);
|
|
75
|
+
inThrottle = true;
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
inThrottle = false;
|
|
78
|
+
if (lastArgs) {
|
|
79
|
+
func.apply(lastContext, lastArgs);
|
|
80
|
+
lastArgs = null;
|
|
81
|
+
lastContext = null;
|
|
82
|
+
}
|
|
83
|
+
}, limit);
|
|
84
|
+
} else {
|
|
85
|
+
lastArgs = args;
|
|
86
|
+
lastContext = this;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Memory-safe rate limiter for periodic operations
|
|
93
|
+
*/
|
|
94
|
+
export class RateLimiter {
|
|
95
|
+
private lastExecution = 0;
|
|
96
|
+
private pendingExecution: (() => void) | null = null;
|
|
97
|
+
|
|
98
|
+
constructor(private readonly minInterval: number) {}
|
|
99
|
+
|
|
100
|
+
async execute(fn: () => void | Promise<void>): Promise<void> {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const timeSinceLastExecution = now - this.lastExecution;
|
|
103
|
+
|
|
104
|
+
if (timeSinceLastExecution >= this.minInterval) {
|
|
105
|
+
this.lastExecution = now;
|
|
106
|
+
await fn();
|
|
107
|
+
} else {
|
|
108
|
+
// Schedule for later if not already pending
|
|
109
|
+
if (!this.pendingExecution) {
|
|
110
|
+
this.pendingExecution = fn;
|
|
111
|
+
const delay = this.minInterval - timeSinceLastExecution;
|
|
112
|
+
setTimeout(async () => {
|
|
113
|
+
if (this.pendingExecution) {
|
|
114
|
+
await this.pendingExecution();
|
|
115
|
+
this.pendingExecution = null;
|
|
116
|
+
}
|
|
117
|
+
this.lastExecution = Date.now();
|
|
118
|
+
}, delay);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
clearPending(): void {
|
|
124
|
+
this.pendingExecution = null;
|
|
125
|
+
}
|
|
30
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Memory-safe periodic task scheduler
|
|
130
|
+
*/
|
|
131
|
+
export class PeriodicTask {
|
|
132
|
+
private timerId: ReturnType<typeof setInterval> | null = null;
|
|
133
|
+
private isRunning = false;
|
|
134
|
+
|
|
135
|
+
constructor(
|
|
136
|
+
private readonly task: () => void | Promise<void>,
|
|
137
|
+
private readonly interval: number
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
start(): void {
|
|
141
|
+
if (this.isRunning) return;
|
|
142
|
+
|
|
143
|
+
this.isRunning = true;
|
|
144
|
+
this.timerId = setInterval(async () => {
|
|
145
|
+
try {
|
|
146
|
+
await this.task();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
Logger.error('Periodic task error', error);
|
|
149
|
+
}
|
|
150
|
+
}, this.interval);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stop(): void {
|
|
154
|
+
if (this.timerId) {
|
|
155
|
+
clearInterval(this.timerId);
|
|
156
|
+
this.timerId = null;
|
|
157
|
+
}
|
|
158
|
+
this.isRunning = false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isActive(): boolean {
|
|
162
|
+
return this.isRunning;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* WeakMap-based cache for automatic garbage collection
|
|
168
|
+
*/
|
|
169
|
+
export class AutoGCWeakCache<K extends object, V> {
|
|
170
|
+
private cache = new WeakMap<K, V>();
|
|
171
|
+
|
|
172
|
+
set(key: K, value: V): void {
|
|
173
|
+
this.cache.set(key, value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get(key: K): V | undefined {
|
|
177
|
+
return this.cache.get(key);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
has(key: K): boolean {
|
|
181
|
+
return this.cache.has(key);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
delete(key: K): boolean {
|
|
185
|
+
return this.cache.delete(key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|