@umituz/react-native-sound 1.2.32 → 1.2.34
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 +1 -1
- package/src/application/interfaces/ISoundCache.ts +4 -6
- package/src/domain/value-objects.ts +128 -0
- package/src/index.ts +35 -26
- package/src/infrastructure/AudioConfig.ts +0 -1
- package/src/infrastructure/AudioRepository.ts +216 -0
- package/src/presentation/SoundStore.ts +156 -0
- package/src/presentation/useSound.ts +183 -0
- package/src/types.ts +1 -2
- package/src/utils.ts +167 -5
- package/src/application/SoundServiceFacade.ts +0 -112
- package/src/application/presenters/SoundPresenter.ts +0 -78
- package/src/application/use-cases/BaseUseCase.ts +0 -36
- package/src/application/use-cases/ClearCacheUseCase.ts +0 -16
- package/src/application/use-cases/PauseSoundUseCase.ts +0 -26
- package/src/application/use-cases/PlaySoundUseCase.ts +0 -106
- package/src/application/use-cases/PreloadSoundUseCase.ts +0 -56
- package/src/application/use-cases/ResumeSoundUseCase.ts +0 -25
- package/src/application/use-cases/SeekSoundUseCase.ts +0 -26
- package/src/application/use-cases/SetRateUseCase.ts +0 -30
- package/src/application/use-cases/SetVolumeUseCase.ts +0 -30
- package/src/application/use-cases/StopSoundUseCase.ts +0 -27
- package/src/domain/entities/SoundState.ts +0 -83
- package/src/domain/value-objects/PlaybackPosition.ts +0 -30
- package/src/domain/value-objects/Rate.ts +0 -26
- package/src/domain/value-objects/SoundId.ts +0 -22
- package/src/domain/value-objects/SoundSource.ts +0 -30
- package/src/domain/value-objects/Volume.ts +0 -26
- package/src/infrastructure/ExpoAudioService.ts +0 -88
- package/src/infrastructure/SoundCache.ts +0 -70
- package/src/presentation/hooks/useSound.ts +0 -47
- package/src/presentation/store.ts +0 -58
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Utility Functions - Backward Compatibility
|
|
2
|
+
* Utility Functions - Backward Compatibility + Performance Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Volume } from './domain/value-objects
|
|
6
|
-
import { Rate } from './domain/value-objects
|
|
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';
|
|
9
|
+
|
|
10
|
+
// ===== Backward Compatibility Functions =====
|
|
7
11
|
|
|
8
12
|
export function clampVolume(volume: number): number {
|
|
9
13
|
return new Volume(volume).getValue();
|
|
@@ -17,10 +21,168 @@ export function validateSoundId(id: string): boolean {
|
|
|
17
21
|
return typeof id === 'string' && id.trim().length > 0;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
export function isSoundSourceValid(source:
|
|
24
|
+
export function isSoundSourceValid(source: unknown): boolean {
|
|
21
25
|
return source !== null && source !== undefined;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
export function isPlaybackStatusSuccess(status:
|
|
28
|
+
export function isPlaybackStatusSuccess(status: PlaybackStatus): boolean {
|
|
25
29
|
return status.isLoaded === true;
|
|
26
30
|
}
|
|
31
|
+
|
|
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
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
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
|
+
}
|
|
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
|
+
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sound Service Facade - Simplified Interface
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { AudioConfig } from '../infrastructure/AudioConfig';
|
|
6
|
-
import { ExpoAudioService } from '../infrastructure/ExpoAudioService';
|
|
7
|
-
import { SoundCache } from '../infrastructure/SoundCache';
|
|
8
|
-
import { SoundPresenter } from './presenters/SoundPresenter';
|
|
9
|
-
import { PlaySoundUseCase } from './use-cases/PlaySoundUseCase';
|
|
10
|
-
import { PauseSoundUseCase } from './use-cases/PauseSoundUseCase';
|
|
11
|
-
import { ResumeSoundUseCase } from './use-cases/ResumeSoundUseCase';
|
|
12
|
-
import { StopSoundUseCase } from './use-cases/StopSoundUseCase';
|
|
13
|
-
import { SeekSoundUseCase } from './use-cases/SeekSoundUseCase';
|
|
14
|
-
import { SetVolumeUseCase } from './use-cases/SetVolumeUseCase';
|
|
15
|
-
import { SetRateUseCase } from './use-cases/SetRateUseCase';
|
|
16
|
-
import { PreloadSoundUseCase } from './use-cases/PreloadSoundUseCase';
|
|
17
|
-
import { ClearCacheUseCase } from './use-cases/ClearCacheUseCase';
|
|
18
|
-
|
|
19
|
-
export class SoundServiceFacade {
|
|
20
|
-
private static instance: SoundServiceFacade | null = null;
|
|
21
|
-
|
|
22
|
-
private readonly audioService = new ExpoAudioService();
|
|
23
|
-
private readonly cache = new SoundCache(this.audioService);
|
|
24
|
-
private readonly presenter: SoundPresenter;
|
|
25
|
-
|
|
26
|
-
private readonly playUseCase: PlaySoundUseCase;
|
|
27
|
-
private readonly pauseUseCase: PauseSoundUseCase;
|
|
28
|
-
private readonly resumeUseCase: ResumeSoundUseCase;
|
|
29
|
-
private readonly stopUseCase: StopSoundUseCase;
|
|
30
|
-
private readonly seekUseCase: SeekSoundUseCase;
|
|
31
|
-
private readonly setVolumeUseCase: SetVolumeUseCase;
|
|
32
|
-
private readonly setRateUseCase: SetRateUseCase;
|
|
33
|
-
private readonly preloadUseCase: PreloadSoundUseCase;
|
|
34
|
-
private readonly clearCacheUseCase: ClearCacheUseCase;
|
|
35
|
-
|
|
36
|
-
private constructor(store: SoundPresenter['store']) {
|
|
37
|
-
AudioConfig.configure().catch(() => {});
|
|
38
|
-
|
|
39
|
-
this.presenter = new SoundPresenter(store);
|
|
40
|
-
|
|
41
|
-
this.playUseCase = new PlaySoundUseCase(this.audioService, this.cache, this.presenter);
|
|
42
|
-
this.pauseUseCase = new PauseSoundUseCase(this.audioService, this.presenter);
|
|
43
|
-
this.resumeUseCase = new ResumeSoundUseCase(this.audioService, this.presenter);
|
|
44
|
-
this.stopUseCase = new StopSoundUseCase(this.audioService, this.presenter);
|
|
45
|
-
this.seekUseCase = new SeekSoundUseCase(this.audioService);
|
|
46
|
-
this.setVolumeUseCase = new SetVolumeUseCase(this.audioService, this.presenter);
|
|
47
|
-
this.setRateUseCase = new SetRateUseCase(this.audioService, this.presenter);
|
|
48
|
-
this.preloadUseCase = new PreloadSoundUseCase(this.audioService, this.cache);
|
|
49
|
-
this.clearCacheUseCase = new ClearCacheUseCase(this.cache);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
static create(store: SoundPresenter['store']): SoundServiceFacade {
|
|
53
|
-
if (!this.instance) {
|
|
54
|
-
this.instance = new SoundServiceFacade(store);
|
|
55
|
-
}
|
|
56
|
-
return this.instance;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async play(id: string, source: any, options?: any): Promise<void> {
|
|
60
|
-
await this.playUseCase.execute({ id, source, ...options });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async pause(): Promise<void> {
|
|
64
|
-
const sound = this.playUseCase.getCurrentSound();
|
|
65
|
-
await this.pauseUseCase.execute({ sound });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async resume(): Promise<void> {
|
|
69
|
-
const sound = this.playUseCase.getCurrentSound();
|
|
70
|
-
await this.resumeUseCase.execute({ sound });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async stop(): Promise<void> {
|
|
74
|
-
const sound = this.playUseCase.getCurrentSound();
|
|
75
|
-
await this.stopUseCase.execute({ sound });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async seek(positionMillis: number): Promise<void> {
|
|
79
|
-
const sound = this.playUseCase.getCurrentSound();
|
|
80
|
-
await this.seekUseCase.execute({ sound, positionMillis });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async setVolume(volume: number): Promise<void> {
|
|
84
|
-
const sound = this.playUseCase.getCurrentSound();
|
|
85
|
-
await this.setVolumeUseCase.execute({ sound, volume });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async setRate(rate: number): Promise<void> {
|
|
89
|
-
const sound = this.playUseCase.getCurrentSound();
|
|
90
|
-
await this.setRateUseCase.execute({ sound, rate });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async preload(id: string, source: any, options?: any): Promise<void> {
|
|
94
|
-
await this.preloadUseCase.execute({ id, source, ...options });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async unload(): Promise<void> {
|
|
98
|
-
await this.playUseCase.unload();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
clearCache(): void {
|
|
102
|
-
this.clearCacheUseCase.execute();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
isCached(id: string): boolean {
|
|
106
|
-
return this.cache.has(id);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
getCurrentId(): string | null {
|
|
110
|
-
return this.playUseCase.getCurrentId();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sound Presenter - Manages Store Updates
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { PlaybackStatus } from '../interfaces/IAudioService';
|
|
6
|
-
import { SoundSourceValue } from '../../domain/value-objects/SoundSource';
|
|
7
|
-
import { Logger } from '../../infrastructure/Logger';
|
|
8
|
-
|
|
9
|
-
export interface SoundStore {
|
|
10
|
-
setPlaying: (isPlaying: boolean) => void;
|
|
11
|
-
setBuffering: (isBuffering: boolean) => void;
|
|
12
|
-
setProgress: (position: number, duration: number) => void;
|
|
13
|
-
setError: (error: string | null) => void;
|
|
14
|
-
setCurrent: (id: string | null, source: SoundSourceValue | null) => void;
|
|
15
|
-
setVolumeState: (volume: number) => void;
|
|
16
|
-
setRateState: (rate: number) => void;
|
|
17
|
-
setLooping: (isLooping: boolean) => void;
|
|
18
|
-
reset: () => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class SoundPresenter {
|
|
22
|
-
constructor(private readonly store: SoundStore) {}
|
|
23
|
-
|
|
24
|
-
onPlaybackUpdate(status: PlaybackStatus): void {
|
|
25
|
-
if (!status.isLoaded && status.error) {
|
|
26
|
-
this.store.setError(status.error);
|
|
27
|
-
this.store.setPlaying(false);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (status.isLoaded) {
|
|
32
|
-
this.store.setPlaying(status.isPlaying);
|
|
33
|
-
this.store.setBuffering(status.isBuffering);
|
|
34
|
-
this.store.setProgress(status.positionMillis, status.durationMillis);
|
|
35
|
-
|
|
36
|
-
if (status.rate !== undefined) {
|
|
37
|
-
this.store.setRateState(status.rate);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (status.didJustFinish && !status.isLooping) {
|
|
41
|
-
this.store.setPlaying(false);
|
|
42
|
-
this.store.setProgress(status.durationMillis, status.durationMillis);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
onPlaybackStart(id: string, source: SoundSourceValue, options: { volume: number; rate: number; isLooping: boolean }): void {
|
|
48
|
-
this.store.setCurrent(id, source);
|
|
49
|
-
this.store.setError(null);
|
|
50
|
-
this.store.setVolumeState(options.volume);
|
|
51
|
-
this.store.setRateState(options.rate);
|
|
52
|
-
this.store.setLooping(options.isLooping);
|
|
53
|
-
Logger.debug(`Playback started: ${id}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
onPlaybackError(error: string): void {
|
|
57
|
-
this.store.setError(error);
|
|
58
|
-
Logger.error('Playback error', error);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
onPlaybackStop(): void {
|
|
62
|
-
this.store.setPlaying(false);
|
|
63
|
-
Logger.debug('Playback stopped');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
onVolumeChange(volume: number): void {
|
|
67
|
-
this.store.setVolumeState(volume);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
onRateChange(rate: number): void {
|
|
71
|
-
this.store.setRateState(rate);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
onReset(): void {
|
|
75
|
-
this.store.reset();
|
|
76
|
-
Logger.debug('State reset');
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base Use Case - Common Error Handling
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { SoundError } from '../../domain/errors/SoundError';
|
|
6
|
-
import { Logger } from '../../infrastructure/Logger';
|
|
7
|
-
|
|
8
|
-
export abstract class BaseUseCase<TInput, TOutput> {
|
|
9
|
-
async execute(input: TInput): Promise<TOutput> {
|
|
10
|
-
try {
|
|
11
|
-
return await this.executeImpl(input);
|
|
12
|
-
} catch (error) {
|
|
13
|
-
if (error instanceof SoundError) {
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
throw SoundError.playbackFailed(error);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
protected abstract executeImpl(input: TInput): Promise<TOutput>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export abstract class BaseVoidUseCase<TInput> {
|
|
24
|
-
async execute(input: TInput): Promise<void> {
|
|
25
|
-
try {
|
|
26
|
-
await this.executeImpl(input);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (error instanceof SoundError) {
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
throw SoundError.playbackFailed(error);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
protected abstract executeImpl(input: TInput): Promise<void>;
|
|
36
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Clear Cache Use Case
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { BaseVoidUseCase } from './BaseUseCase';
|
|
6
|
-
import type { ISoundCache } from '../interfaces/ISoundCache';
|
|
7
|
-
|
|
8
|
-
export class ClearCacheUseCase extends BaseVoidUseCase<void> {
|
|
9
|
-
constructor(private readonly cache: ISoundCache) {
|
|
10
|
-
super();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
protected async executeImpl(): Promise<void> {
|
|
14
|
-
this.cache.clear();
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pause Sound Use Case
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { BaseVoidUseCase } from './BaseUseCase';
|
|
6
|
-
import type { IAudioService } from '../interfaces/IAudioService';
|
|
7
|
-
import { SoundPresenter } from '../presenters/SoundPresenter';
|
|
8
|
-
|
|
9
|
-
interface PauseSoundInput {
|
|
10
|
-
sound: unknown;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class PauseSoundUseCase extends BaseVoidUseCase<PauseSoundInput> {
|
|
14
|
-
constructor(
|
|
15
|
-
private readonly audioService: IAudioService,
|
|
16
|
-
private readonly presenter: SoundPresenter
|
|
17
|
-
) {
|
|
18
|
-
super();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
protected async executeImpl(input: PauseSoundInput): Promise<void> {
|
|
22
|
-
if (!input.sound) return;
|
|
23
|
-
await this.audioService.pause(input.sound);
|
|
24
|
-
this.presenter.onPlaybackStop();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Play Sound Use Case
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { BaseVoidUseCase } from './BaseUseCase';
|
|
6
|
-
import { SoundId } from '../../domain/value-objects/SoundId';
|
|
7
|
-
import { SoundSource } from '../../domain/value-objects/SoundSource';
|
|
8
|
-
import { Volume } from '../../domain/value-objects/Volume';
|
|
9
|
-
import { Rate } from '../../domain/value-objects/Rate';
|
|
10
|
-
import type { IAudioService } from '../interfaces/IAudioService';
|
|
11
|
-
import type { ISoundCache } from '../interfaces/ISoundCache';
|
|
12
|
-
import { SoundPresenter } from '../presenters/SoundPresenter';
|
|
13
|
-
|
|
14
|
-
interface PlaySoundInput {
|
|
15
|
-
id: string;
|
|
16
|
-
source: number | { uri: string; headers?: Record<string, string> };
|
|
17
|
-
volume?: number;
|
|
18
|
-
rate?: number;
|
|
19
|
-
isLooping?: boolean;
|
|
20
|
-
positionMillis?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class PlaySoundUseCase extends BaseVoidUseCase<PlaySoundInput> {
|
|
24
|
-
private currentSound: unknown | null = null;
|
|
25
|
-
private currentId: string | null = null;
|
|
26
|
-
|
|
27
|
-
constructor(
|
|
28
|
-
private readonly audioService: IAudioService,
|
|
29
|
-
private readonly cache: ISoundCache,
|
|
30
|
-
private readonly presenter: SoundPresenter
|
|
31
|
-
) {
|
|
32
|
-
super();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
protected async executeImpl(input: PlaySoundInput): Promise<void> {
|
|
36
|
-
const soundId = new SoundId(input.id);
|
|
37
|
-
const soundSource = new SoundSource(input.source);
|
|
38
|
-
const volume = new Volume(input.volume ?? 1.0);
|
|
39
|
-
const rate = new Rate(input.rate ?? 1.0);
|
|
40
|
-
|
|
41
|
-
// Toggle if same sound
|
|
42
|
-
if (this.currentId === soundId.toString() && this.currentSound) {
|
|
43
|
-
const status = await this.audioService.getStatus(this.currentSound);
|
|
44
|
-
if (status?.isPlaying) {
|
|
45
|
-
await this.audioService.pause(this.currentSound);
|
|
46
|
-
this.presenter.onPlaybackStop();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Unload current
|
|
52
|
-
if (this.currentSound) {
|
|
53
|
-
await this.audioService.unload(this.currentSound);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Try cache first
|
|
57
|
-
const cached = this.cache.get(soundId.toString());
|
|
58
|
-
if (cached) {
|
|
59
|
-
this.currentSound = cached.sound;
|
|
60
|
-
this.cache.delete(soundId.toString());
|
|
61
|
-
} else {
|
|
62
|
-
const { sound } = await this.audioService.createSound(
|
|
63
|
-
soundSource.getValue(),
|
|
64
|
-
{
|
|
65
|
-
shouldPlay: true,
|
|
66
|
-
isLooping: input.isLooping ?? false,
|
|
67
|
-
volume: volume.getValue(),
|
|
68
|
-
rate: rate.getValue(),
|
|
69
|
-
positionMillis: input.positionMillis,
|
|
70
|
-
},
|
|
71
|
-
(status) => this.presenter.onPlaybackUpdate(status)
|
|
72
|
-
);
|
|
73
|
-
this.currentSound = sound;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
await this.audioService.play(this.currentSound);
|
|
77
|
-
this.currentId = soundId.toString();
|
|
78
|
-
|
|
79
|
-
this.presenter.onPlaybackStart(
|
|
80
|
-
soundId.toString(),
|
|
81
|
-
soundSource.getValue(),
|
|
82
|
-
{
|
|
83
|
-
volume: volume.getValue(),
|
|
84
|
-
rate: rate.getValue(),
|
|
85
|
-
isLooping: input.isLooping ?? false,
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
getCurrentSound(): unknown | null {
|
|
91
|
-
return this.currentSound;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
getCurrentId(): string | null {
|
|
95
|
-
return this.currentId;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async unload(): Promise<void> {
|
|
99
|
-
if (this.currentSound) {
|
|
100
|
-
await this.audioService.unload(this.currentSound);
|
|
101
|
-
}
|
|
102
|
-
this.currentSound = null;
|
|
103
|
-
this.currentId = null;
|
|
104
|
-
this.presenter.onReset();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preload Sound Use Case
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { BaseVoidUseCase } from './BaseUseCase';
|
|
6
|
-
import { SoundId } from '../../domain/value-objects/SoundId';
|
|
7
|
-
import { SoundSource } from '../../domain/value-objects/SoundSource';
|
|
8
|
-
import { Volume } from '../../domain/value-objects/Volume';
|
|
9
|
-
import { Rate } from '../../domain/value-objects/Rate';
|
|
10
|
-
import type { IAudioService } from '../interfaces/IAudioService';
|
|
11
|
-
import type { ISoundCache } from '../interfaces/ISoundCache';
|
|
12
|
-
|
|
13
|
-
interface PreloadSoundInput {
|
|
14
|
-
id: string;
|
|
15
|
-
source: number | { uri: string; headers?: Record<string, string> };
|
|
16
|
-
volume?: number;
|
|
17
|
-
rate?: number;
|
|
18
|
-
isLooping?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class PreloadSoundUseCase extends BaseVoidUseCase<PreloadSoundInput> {
|
|
22
|
-
constructor(
|
|
23
|
-
private readonly audioService: IAudioService,
|
|
24
|
-
private readonly cache: ISoundCache
|
|
25
|
-
) {
|
|
26
|
-
super();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
protected async executeImpl(input: PreloadSoundInput): Promise<void> {
|
|
30
|
-
const soundId = new SoundId(input.id);
|
|
31
|
-
const soundSource = new SoundSource(input.source);
|
|
32
|
-
const volume = new Volume(input.volume ?? 1.0);
|
|
33
|
-
const rate = new Rate(input.rate ?? 1.0);
|
|
34
|
-
|
|
35
|
-
if (this.cache.has(soundId.toString())) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const { sound } = await this.audioService.createSound(
|
|
40
|
-
soundSource.getValue(),
|
|
41
|
-
{
|
|
42
|
-
shouldPlay: false,
|
|
43
|
-
isLooping: input.isLooping ?? false,
|
|
44
|
-
volume: volume.getValue(),
|
|
45
|
-
rate: rate.getValue(),
|
|
46
|
-
},
|
|
47
|
-
() => {}
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
this.cache.set(soundId.toString(), {
|
|
51
|
-
sound,
|
|
52
|
-
source: soundSource.getValue(),
|
|
53
|
-
loadedAt: Date.now(),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resume Sound Use Case
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { BaseVoidUseCase } from './BaseUseCase';
|
|
6
|
-
import type { IAudioService } from '../interfaces/IAudioService';
|
|
7
|
-
import { SoundPresenter } from '../presenters/SoundPresenter';
|
|
8
|
-
|
|
9
|
-
interface ResumeSoundInput {
|
|
10
|
-
sound: unknown;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class ResumeSoundUseCase extends BaseVoidUseCase<ResumeSoundInput> {
|
|
14
|
-
constructor(
|
|
15
|
-
private readonly audioService: IAudioService,
|
|
16
|
-
private readonly presenter: SoundPresenter
|
|
17
|
-
) {
|
|
18
|
-
super();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
protected async executeImpl(input: ResumeSoundInput): Promise<void> {
|
|
22
|
-
if (!input.sound) return;
|
|
23
|
-
await this.audioService.play(input.sound);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Seek Sound Use Case
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { BaseVoidUseCase } from './BaseUseCase';
|
|
6
|
-
import { PlaybackPosition } from '../../domain/value-objects/PlaybackPosition';
|
|
7
|
-
import type { IAudioService } from '../interfaces/IAudioService';
|
|
8
|
-
|
|
9
|
-
interface SeekSoundInput {
|
|
10
|
-
sound: unknown;
|
|
11
|
-
positionMillis: number;
|
|
12
|
-
durationMillis?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class SeekSoundUseCase extends BaseVoidUseCase<SeekSoundInput> {
|
|
16
|
-
constructor(private readonly audioService: IAudioService) {
|
|
17
|
-
super();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
protected async executeImpl(input: SeekSoundInput): Promise<void> {
|
|
21
|
-
if (!input.sound) return;
|
|
22
|
-
|
|
23
|
-
const position = new PlaybackPosition(input.positionMillis, input.durationMillis);
|
|
24
|
-
await this.audioService.setPosition(input.sound, position.getValue());
|
|
25
|
-
}
|
|
26
|
-
}
|