96 lines
2.6 KiB
TypeScript
96 lines
2.6 KiB
TypeScript
import { useEffect, useRef, useCallback } from 'react';
|
|
import { Audio, AVPlaybackStatus } from 'expo-av';
|
|
import { usePlayer } from '../store';
|
|
|
|
let _sound: Audio.Sound | null = null;
|
|
|
|
// Module-level singleton so the sound survives re-renders
|
|
export async function getSound() { return _sound; }
|
|
|
|
export function useAudio() {
|
|
const initialized = useRef(false);
|
|
const {
|
|
queue, index, playing,
|
|
setPlaying, setProgress, setDuration,
|
|
nextTrack, setIndex, repeat,
|
|
} = usePlayer();
|
|
|
|
const currentTrack = queue[index] ?? null;
|
|
|
|
// Init audio session once
|
|
useEffect(() => {
|
|
if (initialized.current) return;
|
|
initialized.current = true;
|
|
Audio.setAudioModeAsync({
|
|
staysActiveInBackground: true,
|
|
playsInSilentModeIOS: true,
|
|
interruptionModeIOS: 1,
|
|
shouldDuckAndroid: true,
|
|
});
|
|
}, []);
|
|
|
|
// Load new track whenever index/id changes
|
|
useEffect(() => {
|
|
if (!currentTrack) return;
|
|
|
|
const load = async () => {
|
|
// Unload previous
|
|
if (_sound) {
|
|
await _sound.stopAsync().catch(() => {});
|
|
await _sound.unloadAsync().catch(() => {});
|
|
_sound = null;
|
|
}
|
|
|
|
const { sound } = await Audio.Sound.createAsync(
|
|
{ uri: currentTrack.stream_url },
|
|
{ shouldPlay: true, progressUpdateIntervalMillis: 500 },
|
|
(status: AVPlaybackStatus) => {
|
|
if (!status.isLoaded) return;
|
|
setProgress(status.positionMillis / 1000);
|
|
setDuration((status.durationMillis ?? 0) / 1000);
|
|
if (status.isPlaying !== usePlayer.getState().playing) {
|
|
setPlaying(status.isPlaying);
|
|
}
|
|
if (status.didJustFinish) {
|
|
const state = usePlayer.getState();
|
|
const ni = state.nextTrack();
|
|
if (ni !== state.index || state.repeat === 2) {
|
|
setIndex(ni);
|
|
} else {
|
|
setPlaying(false);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
_sound = sound;
|
|
};
|
|
|
|
load().catch(console.error);
|
|
}, [currentTrack?.id]);
|
|
|
|
const togglePlay = useCallback(async () => {
|
|
if (!_sound) return;
|
|
const status = await _sound.getStatusAsync();
|
|
if (!status.isLoaded) return;
|
|
if (status.isPlaying) {
|
|
await _sound.pauseAsync();
|
|
} else {
|
|
await _sound.playAsync();
|
|
}
|
|
}, []);
|
|
|
|
const seek = useCallback(async (seconds: number) => {
|
|
if (!_sound) return;
|
|
await _sound.setPositionAsync(seconds * 1000);
|
|
}, []);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
_sound?.unloadAsync().catch(() => {});
|
|
_sound = null;
|
|
};
|
|
}, []);
|
|
|
|
return { togglePlay, seek };
|
|
}
|