@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.
- package/package.json +1 -5
- package/src/application/SoundServiceFacade.ts +112 -0
- package/src/application/interfaces/IAudioService.ts +42 -0
- package/src/application/interfaces/ISoundCache.ts +21 -0
- package/src/application/presenters/SoundPresenter.ts +78 -0
- package/src/application/use-cases/BaseUseCase.ts +36 -0
- package/src/application/use-cases/ClearCacheUseCase.ts +16 -0
- package/src/application/use-cases/PauseSoundUseCase.ts +26 -0
- package/src/application/use-cases/PlaySoundUseCase.ts +106 -0
- package/src/application/use-cases/PreloadSoundUseCase.ts +56 -0
- package/src/application/use-cases/ResumeSoundUseCase.ts +25 -0
- package/src/application/use-cases/SeekSoundUseCase.ts +26 -0
- package/src/application/use-cases/SetRateUseCase.ts +30 -0
- package/src/application/use-cases/SetVolumeUseCase.ts +30 -0
- package/src/application/use-cases/StopSoundUseCase.ts +27 -0
- package/src/domain/entities/SoundState.ts +83 -0
- package/src/domain/errors/SoundError.ts +44 -0
- package/src/domain/value-objects/PlaybackPosition.ts +30 -0
- package/src/domain/value-objects/Rate.ts +26 -0
- package/src/domain/value-objects/SoundId.ts +22 -0
- package/src/domain/value-objects/SoundSource.ts +30 -0
- package/src/domain/value-objects/Volume.ts +26 -0
- package/src/index.ts +36 -6
- package/src/infrastructure/AudioConfig.ts +25 -0
- package/src/infrastructure/ExpoAudioService.ts +88 -0
- package/src/infrastructure/Logger.ts +23 -0
- package/src/infrastructure/SoundCache.ts +70 -0
- package/src/presentation/hooks/useSound.ts +47 -0
- package/src/{store.ts → presentation/store.ts} +22 -9
- package/src/types.ts +6 -5
- package/src/utils.ts +13 -17
- package/src/AudioManager.ts +0 -338
- package/src/useSound.ts +0 -107
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound State Entity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SoundSource } from '../value-objects/SoundSource';
|
|
6
|
+
import { Volume } from '../value-objects/Volume';
|
|
7
|
+
import { Rate } from '../value-objects/Rate';
|
|
8
|
+
|
|
9
|
+
export interface SoundStateData {
|
|
10
|
+
isPlaying: boolean;
|
|
11
|
+
isBuffering: boolean;
|
|
12
|
+
positionMillis: number;
|
|
13
|
+
durationMillis: number;
|
|
14
|
+
volume: number;
|
|
15
|
+
rate: number;
|
|
16
|
+
isLooping: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
currentSource: SoundSource | null;
|
|
19
|
+
currentId: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SoundState {
|
|
23
|
+
constructor(private data: SoundStateData) {}
|
|
24
|
+
|
|
25
|
+
isPlaying(): boolean {
|
|
26
|
+
return this.data.isPlaying;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isBuffering(): boolean {
|
|
30
|
+
return this.data.isBuffering;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getPosition(): number {
|
|
34
|
+
return this.data.positionMillis;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getDuration(): number {
|
|
38
|
+
return this.data.durationMillis;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getVolume(): number {
|
|
42
|
+
return this.data.volume;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getRate(): number {
|
|
46
|
+
return this.data.rate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isLooping(): boolean {
|
|
50
|
+
return this.data.isLooping;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getError(): string | null {
|
|
54
|
+
return this.data.error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCurrentSource(): SoundSource | null {
|
|
58
|
+
return this.data.currentSource;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getCurrentId(): string | null {
|
|
62
|
+
return this.data.currentId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toData(): SoundStateData {
|
|
66
|
+
return { ...this.data };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static createEmpty(): SoundState {
|
|
70
|
+
return new SoundState({
|
|
71
|
+
isPlaying: false,
|
|
72
|
+
isBuffering: false,
|
|
73
|
+
positionMillis: 0,
|
|
74
|
+
durationMillis: 0,
|
|
75
|
+
volume: 1.0,
|
|
76
|
+
rate: 1.0,
|
|
77
|
+
isLooping: false,
|
|
78
|
+
error: null,
|
|
79
|
+
currentSource: null,
|
|
80
|
+
currentId: null,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Errors for Sound Operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum SoundErrorCode {
|
|
6
|
+
INVALID_SOUND_ID = 'INVALID_SOUND_ID',
|
|
7
|
+
INVALID_SOUND_SOURCE = 'INVALID_SOUND_SOURCE',
|
|
8
|
+
PLAYBACK_FAILED = 'PLAYBACK_FAILED',
|
|
9
|
+
SOUND_NOT_LOADED = 'SOUND_NOT_LOADED',
|
|
10
|
+
INVALID_POSITION = 'INVALID_POSITION',
|
|
11
|
+
CACHE_ERROR = 'CACHE_ERROR',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SoundError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
public readonly code: SoundErrorCode,
|
|
17
|
+
message: string,
|
|
18
|
+
public readonly originalError?: unknown
|
|
19
|
+
) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'SoundError';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static invalidSoundId(): SoundError {
|
|
25
|
+
return new SoundError(SoundErrorCode.INVALID_SOUND_ID, 'Sound id must be a non-empty string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static invalidSoundSource(): SoundError {
|
|
29
|
+
return new SoundError(SoundErrorCode.INVALID_SOUND_SOURCE, 'Sound source cannot be null or undefined');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static playbackFailed(error: unknown): SoundError {
|
|
33
|
+
const message = error instanceof Error ? error.message : 'Unknown playback error';
|
|
34
|
+
return new SoundError(SoundErrorCode.PLAYBACK_FAILED, message, error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static soundNotLoaded(): SoundError {
|
|
38
|
+
return new SoundError(SoundErrorCode.SOUND_NOT_LOADED, 'Sound is not loaded');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static invalidPosition(): SoundError {
|
|
42
|
+
return new SoundError(SoundErrorCode.INVALID_POSITION, 'Position must be a finite number');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playback Position Value Object
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SoundError } from '../errors/SoundError';
|
|
6
|
+
|
|
7
|
+
export class PlaybackPosition {
|
|
8
|
+
private readonly value: number;
|
|
9
|
+
private readonly duration?: number;
|
|
10
|
+
|
|
11
|
+
constructor(value: number, duration?: number) {
|
|
12
|
+
if (!Number.isFinite(value)) {
|
|
13
|
+
throw SoundError.invalidPosition();
|
|
14
|
+
}
|
|
15
|
+
this.value = duration ? this.clamp(value, duration) : value;
|
|
16
|
+
this.duration = duration;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private clamp(value: number, max: number): number {
|
|
20
|
+
return Math.max(0, Math.min(max, value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getValue(): number {
|
|
24
|
+
return this.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getDuration(): number | undefined {
|
|
28
|
+
return this.duration;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Value Object
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class Rate {
|
|
6
|
+
private readonly MIN = 0.5;
|
|
7
|
+
private readonly MAX = 2.0;
|
|
8
|
+
private readonly value: number;
|
|
9
|
+
|
|
10
|
+
constructor(value: number) {
|
|
11
|
+
this.value = this.clamp(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private clamp(value: number): number {
|
|
15
|
+
if (!Number.isFinite(value)) return 1.0;
|
|
16
|
+
return Math.max(this.MIN, Math.min(this.MAX, value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getValue(): number {
|
|
20
|
+
return this.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
equals(other: Rate): boolean {
|
|
24
|
+
return this.value === other.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound ID Value Object
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class SoundId {
|
|
6
|
+
private readonly value: string;
|
|
7
|
+
|
|
8
|
+
constructor(value: string) {
|
|
9
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
10
|
+
throw new Error('Sound id must be a non-empty string');
|
|
11
|
+
}
|
|
12
|
+
this.value = value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toString(): string {
|
|
16
|
+
return this.value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
equals(other: SoundId): boolean {
|
|
20
|
+
return this.value === other.value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound Source Value Object
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SoundError } from '../errors/SoundError';
|
|
6
|
+
|
|
7
|
+
export type SoundSourceValue = number | { uri: string; headers?: Record<string, string> };
|
|
8
|
+
|
|
9
|
+
export class SoundSource {
|
|
10
|
+
private readonly value: SoundSourceValue;
|
|
11
|
+
|
|
12
|
+
constructor(value: SoundSourceValue | null | undefined) {
|
|
13
|
+
if (value === null || value === undefined) {
|
|
14
|
+
throw SoundError.invalidSoundSource();
|
|
15
|
+
}
|
|
16
|
+
this.value = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getValue(): SoundSourceValue {
|
|
20
|
+
return this.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
isLocal(): boolean {
|
|
24
|
+
return typeof this.value === 'number';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isRemote(): boolean {
|
|
28
|
+
return typeof this.value === 'object' && 'uri' in this.value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volume Value Object
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class Volume {
|
|
6
|
+
private readonly MIN = 0;
|
|
7
|
+
private readonly MAX = 1;
|
|
8
|
+
private readonly value: number;
|
|
9
|
+
|
|
10
|
+
constructor(value: number) {
|
|
11
|
+
this.value = this.clamp(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private clamp(value: number): number {
|
|
15
|
+
if (!Number.isFinite(value)) return 1.0;
|
|
16
|
+
return Math.max(this.MIN, Math.min(this.MAX, value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getValue(): number {
|
|
20
|
+
return this.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
equals(other: Volume): boolean {
|
|
24
|
+
return this.value === other.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-sound
|
|
3
|
+
* DDD-based audio management for React Native
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Public API
|
|
7
|
+
export { useSound } from './presentation/hooks/useSound';
|
|
8
|
+
export { useSoundStore } from './presentation/store';
|
|
9
|
+
|
|
10
|
+
// Types
|
|
11
|
+
export type { PlaybackOptions, SoundState, SoundSource } from './types';
|
|
12
|
+
|
|
13
|
+
// Utilities (backward compatibility)
|
|
2
14
|
export {
|
|
3
|
-
isPlaybackStatusSuccess,
|
|
4
|
-
isSoundSourceValid,
|
|
5
15
|
clampVolume,
|
|
6
16
|
clampRate,
|
|
7
17
|
validateSoundId,
|
|
18
|
+
isSoundSourceValid,
|
|
19
|
+
isPlaybackStatusSuccess,
|
|
8
20
|
} from './utils';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export {
|
|
21
|
+
|
|
22
|
+
// Domain exports (for advanced usage)
|
|
23
|
+
export { SoundError } from './domain/errors/SoundError';
|
|
24
|
+
export { SoundId } from './domain/value-objects/SoundId';
|
|
25
|
+
export { SoundSource as SoundSourceVO } from './domain/value-objects/SoundSource';
|
|
26
|
+
export { Volume } from './domain/value-objects/Volume';
|
|
27
|
+
export { Rate } from './domain/value-objects/Rate';
|
|
28
|
+
export { PlaybackPosition } from './domain/value-objects/PlaybackPosition';
|
|
29
|
+
export { SoundState as SoundStateEntity } from './domain/entities/SoundState';
|
|
30
|
+
|
|
31
|
+
// Infrastructure exports (for testing/customization)
|
|
32
|
+
export { Logger } from './infrastructure/Logger';
|
|
33
|
+
export { AudioConfig } from './infrastructure/AudioConfig';
|
|
34
|
+
export { ExpoAudioService } from './infrastructure/ExpoAudioService';
|
|
35
|
+
export { SoundCache } from './infrastructure/SoundCache';
|
|
36
|
+
|
|
37
|
+
// Application exports (for testing/customization)
|
|
38
|
+
export { SoundServiceFacade } from './application/SoundServiceFacade';
|
|
39
|
+
export type { IAudioService, PlaybackStatus } from './application/interfaces/IAudioService';
|
|
40
|
+
export type { ISoundCache, CachedSound } from './application/interfaces/ISoundCache';
|
|
41
|
+
export { SoundPresenter } from './application/presenters/SoundPresenter';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Configuration Service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Audio, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
|
|
6
|
+
import type { Audio as AudioType } from 'expo-av';
|
|
7
|
+
import { Logger } from './Logger';
|
|
8
|
+
|
|
9
|
+
export class AudioConfig {
|
|
10
|
+
static async configure(): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
await Audio.setAudioModeAsync({
|
|
13
|
+
playsInSilentModeIOS: true,
|
|
14
|
+
staysActiveInBackground: false,
|
|
15
|
+
shouldDuckAndroid: true,
|
|
16
|
+
playThroughEarpieceAndroid: false,
|
|
17
|
+
interruptionModeIOS: InterruptionModeIOS.DuckOthers,
|
|
18
|
+
interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
|
|
19
|
+
});
|
|
20
|
+
Logger.debug('Audio configured successfully');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
Logger.warn('Failed to configure audio session', error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo Audio Service Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Audio } from 'expo-av';
|
|
6
|
+
import type { IAudioService, PlaybackOptions, PlaybackStatus } from '../application/interfaces/IAudioService';
|
|
7
|
+
import { SoundSourceValue } from '../domain/value-objects/SoundSource';
|
|
8
|
+
import { Logger } from './Logger';
|
|
9
|
+
|
|
10
|
+
export class ExpoAudioService implements IAudioService {
|
|
11
|
+
async createSound(
|
|
12
|
+
source: SoundSourceValue,
|
|
13
|
+
options: PlaybackOptions,
|
|
14
|
+
onStatusUpdate: (status: PlaybackStatus) => void
|
|
15
|
+
): Promise<{ sound: Audio.Sound }> {
|
|
16
|
+
const { sound } = await Audio.Sound.createAsync(
|
|
17
|
+
source,
|
|
18
|
+
{
|
|
19
|
+
shouldPlay: options.shouldPlay,
|
|
20
|
+
isLooping: options.isLooping,
|
|
21
|
+
volume: options.volume,
|
|
22
|
+
rate: options.rate,
|
|
23
|
+
positionMillis: options.positionMillis,
|
|
24
|
+
},
|
|
25
|
+
(status) => onStatusUpdate(this.mapStatus(status))
|
|
26
|
+
);
|
|
27
|
+
Logger.debug('Sound created');
|
|
28
|
+
return { sound };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async play(sound: unknown): Promise<void> {
|
|
32
|
+
await (sound as Audio.Sound).playAsync();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async pause(sound: unknown): Promise<void> {
|
|
36
|
+
await (sound as Audio.Sound).pauseAsync();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async stop(sound: unknown): Promise<void> {
|
|
40
|
+
await (sound as Audio.Sound).stopAsync();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async unload(sound: unknown): Promise<void> {
|
|
44
|
+
await (sound as Audio.Sound).unloadAsync();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async setVolume(sound: unknown, volume: number): Promise<void> {
|
|
48
|
+
await (sound as Audio.Sound).setVolumeAsync(volume);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async setRate(sound: unknown, rate: number): Promise<void> {
|
|
52
|
+
await (sound as Audio.Sound).setRateAsync(rate, false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async setPosition(sound: unknown, positionMillis: number): Promise<void> {
|
|
56
|
+
await (sound as Audio.Sound).setStatusAsync({ positionMillis });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getStatus(sound: unknown): Promise<PlaybackStatus | null> {
|
|
60
|
+
const status = await (sound as Audio.Sound).getStatusAsync();
|
|
61
|
+
return status.isLoaded ? this.mapStatus(status) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private mapStatus(status: { isLoaded: boolean } & any): PlaybackStatus {
|
|
65
|
+
if (!status.isLoaded) {
|
|
66
|
+
return {
|
|
67
|
+
isLoaded: false,
|
|
68
|
+
isPlaying: false,
|
|
69
|
+
isBuffering: false,
|
|
70
|
+
positionMillis: 0,
|
|
71
|
+
durationMillis: 0,
|
|
72
|
+
isLooping: false,
|
|
73
|
+
didJustFinish: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
isLoaded: true,
|
|
78
|
+
isPlaying: status.isPlaying,
|
|
79
|
+
isBuffering: status.isBuffering,
|
|
80
|
+
positionMillis: status.positionMillis,
|
|
81
|
+
durationMillis: status.durationMillis || 0,
|
|
82
|
+
rate: status.rate,
|
|
83
|
+
isLooping: status.isLooping,
|
|
84
|
+
didJustFinish: status.didJustFinish,
|
|
85
|
+
error: status.error,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging Service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class Logger {
|
|
6
|
+
static debug(message: string, ...args: unknown[]): void {
|
|
7
|
+
if (__DEV__) {
|
|
8
|
+
console.log(`[Sound] ${message}`, ...args);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static warn(message: string, ...args: unknown[]): void {
|
|
13
|
+
if (__DEV__) {
|
|
14
|
+
console.warn(`[Sound] ${message}`, ...args);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static error(message: string, error?: unknown): void {
|
|
19
|
+
if (__DEV__) {
|
|
20
|
+
console.error(`[Sound] ${message}`, error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound Cache Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ISoundCache, CachedSound } from '../application/interfaces/ISoundCache';
|
|
6
|
+
import { IAudioService } from '../application/interfaces/IAudioService';
|
|
7
|
+
import { Logger } from './Logger';
|
|
8
|
+
|
|
9
|
+
export class SoundCache implements ISoundCache {
|
|
10
|
+
private cache = new Map<string, CachedSound>();
|
|
11
|
+
private readonly maxCacheSize = 3;
|
|
12
|
+
private readonly cacheExpireMs = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
constructor(private readonly audioService: IAudioService) {}
|
|
15
|
+
|
|
16
|
+
get(id: string): CachedSound | undefined {
|
|
17
|
+
return this.cache.get(id);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(id: string, cached: CachedSound): void {
|
|
21
|
+
this.cleanExpired();
|
|
22
|
+
this.enforceLimit();
|
|
23
|
+
this.cache.set(id, cached);
|
|
24
|
+
Logger.debug(`Cached: ${id}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
delete(id: string): void {
|
|
28
|
+
const cached = this.cache.get(id);
|
|
29
|
+
if (cached) {
|
|
30
|
+
this.audioService.unload(cached.sound).catch((error) => {
|
|
31
|
+
Logger.warn(`Failed to unload cache: ${id}`, error);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
this.cache.delete(id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
has(id: string): boolean {
|
|
38
|
+
return this.cache.has(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear(): void {
|
|
42
|
+
for (const cached of this.cache.values()) {
|
|
43
|
+
this.audioService.unload(cached.sound).catch((error) => {
|
|
44
|
+
Logger.warn('Failed to unload during cache clear', error);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
this.cache.clear();
|
|
48
|
+
Logger.debug('Cache cleared');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cleanExpired(): void {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
for (const [id, cached] of this.cache) {
|
|
54
|
+
if (now - cached.loadedAt > this.cacheExpireMs) {
|
|
55
|
+
this.delete(id);
|
|
56
|
+
Logger.debug(`Expired cache removed: ${id}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enforceLimit(): void {
|
|
62
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
63
|
+
const firstKey = this.cache.keys().next().value;
|
|
64
|
+
if (firstKey) {
|
|
65
|
+
this.delete(firstKey);
|
|
66
|
+
Logger.debug(`Cache limit enforced, removed: ${firstKey}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSound Hook - Presentation Layer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useSoundStore } from '../store';
|
|
6
|
+
import { SoundServiceFacade } from '../../application/SoundServiceFacade';
|
|
7
|
+
|
|
8
|
+
let facade: SoundServiceFacade | null = null;
|
|
9
|
+
|
|
10
|
+
const getFacade = (): SoundServiceFacade => {
|
|
11
|
+
if (!facade) {
|
|
12
|
+
facade = SoundServiceFacade.create(useSoundStore.getState());
|
|
13
|
+
}
|
|
14
|
+
return facade;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useSound = () => {
|
|
18
|
+
const state = useSoundStore();
|
|
19
|
+
|
|
20
|
+
const facade = getFacade();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
// State
|
|
24
|
+
isPlaying: state.isPlaying,
|
|
25
|
+
isBuffering: state.isBuffering,
|
|
26
|
+
isLooping: state.isLooping,
|
|
27
|
+
position: state.positionMillis,
|
|
28
|
+
duration: state.durationMillis,
|
|
29
|
+
currentId: state.currentId,
|
|
30
|
+
error: state.error,
|
|
31
|
+
volume: state.volume,
|
|
32
|
+
rate: state.rate,
|
|
33
|
+
|
|
34
|
+
// Actions
|
|
35
|
+
play: (id: string, source: any, options?: any) => facade.play(id, source, options),
|
|
36
|
+
pause: () => facade.pause(),
|
|
37
|
+
resume: () => facade.resume(),
|
|
38
|
+
stop: () => facade.stop(),
|
|
39
|
+
seek: (positionMillis: number) => facade.seek(positionMillis),
|
|
40
|
+
setVolume: (volume: number) => facade.setVolume(volume),
|
|
41
|
+
setRate: (rate: number) => facade.setRate(rate),
|
|
42
|
+
preload: (id: string, source: any, options?: any) => facade.preload(id, source, options),
|
|
43
|
+
unload: () => facade.unload(),
|
|
44
|
+
clearCache: () => facade.clearCache(),
|
|
45
|
+
isCached: (id: string) => facade.isCached(id),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -1,19 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound Store - State Management
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { createStore } from '@umituz/react-native-design-system/storage';
|
|
2
|
-
|
|
6
|
+
|
|
7
|
+
interface SoundState {
|
|
8
|
+
isPlaying: boolean;
|
|
9
|
+
isBuffering: boolean;
|
|
10
|
+
positionMillis: number;
|
|
11
|
+
durationMillis: number;
|
|
12
|
+
volume: number;
|
|
13
|
+
rate: number;
|
|
14
|
+
isLooping: boolean;
|
|
15
|
+
error: string | null;
|
|
16
|
+
currentId: string | null;
|
|
17
|
+
}
|
|
3
18
|
|
|
4
19
|
interface SoundActions {
|
|
5
20
|
setPlaying: (isPlaying: boolean) => void;
|
|
6
21
|
setBuffering: (isBuffering: boolean) => void;
|
|
7
22
|
setProgress: (position: number, duration: number) => void;
|
|
8
23
|
setError: (error: string | null) => void;
|
|
9
|
-
setCurrent: (id: string | null, source:
|
|
24
|
+
setCurrent: (id: string | null, source: any) => void;
|
|
10
25
|
setVolumeState: (volume: number) => void;
|
|
11
26
|
setRateState: (rate: number) => void;
|
|
12
27
|
setLooping: (isLooping: boolean) => void;
|
|
13
28
|
reset: () => void;
|
|
14
29
|
}
|
|
15
30
|
|
|
16
|
-
const
|
|
31
|
+
const initialState: SoundState = {
|
|
17
32
|
isPlaying: false,
|
|
18
33
|
isBuffering: false,
|
|
19
34
|
positionMillis: 0,
|
|
@@ -22,24 +37,22 @@ const initialSoundState: SoundState = {
|
|
|
22
37
|
rate: 1.0,
|
|
23
38
|
isLooping: false,
|
|
24
39
|
error: null,
|
|
25
|
-
currentSource: null,
|
|
26
40
|
currentId: null,
|
|
27
41
|
};
|
|
28
42
|
|
|
29
43
|
export const useSoundStore = createStore<SoundState, SoundActions>({
|
|
30
44
|
name: 'sound-store',
|
|
31
|
-
initialState
|
|
45
|
+
initialState,
|
|
32
46
|
persist: false,
|
|
33
47
|
actions: (set) => ({
|
|
34
48
|
setPlaying: (isPlaying) => set({ isPlaying }),
|
|
35
49
|
setBuffering: (isBuffering) => set({ isBuffering }),
|
|
36
|
-
setProgress: (position, duration) =>
|
|
37
|
-
set({ positionMillis: position, durationMillis: duration }),
|
|
50
|
+
setProgress: (position, duration) => set({ positionMillis: position, durationMillis: duration }),
|
|
38
51
|
setError: (error) => set({ error }),
|
|
39
|
-
setCurrent: (
|
|
52
|
+
setCurrent: (currentId) => set({ currentId }),
|
|
40
53
|
setVolumeState: (volume) => set({ volume }),
|
|
41
54
|
setRateState: (rate) => set({ rate }),
|
|
42
55
|
setLooping: (isLooping) => set({ isLooping }),
|
|
43
|
-
reset: () => set(
|
|
56
|
+
reset: () => set(initialState),
|
|
44
57
|
}),
|
|
45
58
|
});
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
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/SoundSource';
|
|
6
|
+
export type { SoundStateData } from './domain/entities/SoundState';
|
|
8
7
|
|
|
9
8
|
export interface PlaybackOptions {
|
|
10
9
|
isLooping?: boolean;
|
|
@@ -22,6 +21,8 @@ export interface SoundState {
|
|
|
22
21
|
rate: number;
|
|
23
22
|
isLooping: boolean;
|
|
24
23
|
error: string | null;
|
|
25
|
-
currentSource:
|
|
24
|
+
currentSource: any;
|
|
26
25
|
currentId: string | null;
|
|
27
26
|
}
|
|
27
|
+
|
|
28
|
+
export type SoundSource = number | { uri: string; headers?: Record<string, string> } | null;
|