-
Notifications
You must be signed in to change notification settings - Fork 100
Description
When using react-native-sound-player on iOS, seeking the audio causes the timer to freeze in the last few seconds. Even after the audio has finished playing, the current time displayed remains 5–20 seconds behind the actual end. This issue does not occur on Android.
Steps to Reproduce
Load an audio file using react-native-sound-player on iOS.
Start playback and seek to any position close to the end of the audio.
Observe that the current time stops updating correctly even after the audio finishes.
Expected Behavior
The current time should accurately reflect the audio progress and reach the total duration when the audio finishes.
Actual Behavior
On iOS, the current time freezes several seconds before the audio actually ends, making it appear as if there’s still playback left.
Platform:
OS: iOS
React Native Version: 0.78.1
Library Version: 0.14.5
Additional Context
Issue only occurs when seeking near the end of the audio.
Works correctly on Android.
import {useCallback, useEffect, useRef, useState} from 'react';
import SoundPlayer from 'react-native-sound-player';
import BackgroundTimer from 'react-native-background-timer';
import {AppState, AppStateStatus} from 'react-native';
import {ErrorHandler} from '../utils/ErrorHandler';
import {useSelector} from 'react-redux';
import {stateUserDetails} from '../store/auth';
import Sound from 'react-native-sound';
import {isIOS} from '../constant/deviceInfo';
import {
activateKeepAwake,
deactivateKeepAwake,
} from '@sayem314/react-native-keep-awake';
export interface SoundTimerConfig {
timerInterval?: number;
isFocused?: boolean;
duration?: number;
}
export const useSoundTimer = (config: SoundTimerConfig = {}) => {
const {timerInterval = 1000, isFocused, duration = 0} = config;
const selfInfo = useSelector(stateUserDetails);
const appState = useRef(AppState.currentState);
const [isPlaying, setIsPlaying] = useState(false);
const [timer, setTimer] = useState(duration);
const [isTimerCompleted, setIsTimerCompleted] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [isPause, setIsPause] = useState();
const syncIntervalRef = useRef<number | null>(null);
const countdownIntervalRef = useRef<number | null>(null);
const currentAudioUrl = useRef<string | null>(null);
const beepSound = useRef<Sound | null>(null);
const audioCompleted = useRef(false);
const audioLoaded = useRef(false);
const beepCount = useRef(0);
const audioLoadedMounted = useRef(true);
const initializeSounds = useCallback(() => {
try {
if (isIOS) {
Sound.setCategory('Playback', true);
}
if (selfInfo?.next_practice_sound && !beepSound.current) {
beepSound.current = new Sound(
selfInfo.beep_next_practice_sound,
'',
error => {
if (error) {
ErrorHandler(error);
} else {
beepSound.current?.setVolume(isMuted ? 0 : 1);
}
},
);
}
} catch (error) {
ErrorHandler(error);
}
}, [selfInfo, isMuted]);
const routineCompleteSound = useCallback(() => {
const soundUrl = selfInfo?.routine_complete_beep_sound;
if (!soundUrl) {
return;
}
const sound = new Sound(soundUrl, '', error => {
if (error) {
ErrorHandler(error);
return;
}
sound.play(_success => {
sound.release();
});
});
}, [selfInfo?.routine_complete_beep_sound]);
const routineBeepSound = useCallback(() => {
if (beepSound.current) {
beepSound.current.setCurrentTime(0);
beepSound.current.play();
}
}, []);
const stopAllTimers = useCallback(() => {
if (syncIntervalRef.current) {
BackgroundTimer.clearInterval(syncIntervalRef.current);
syncIntervalRef.current = null;
}
if (countdownIntervalRef.current) {
BackgroundTimer.clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
}
deactivateKeepAwake();
}, []);
const stopSound = useCallback(() => {
try {
SoundPlayer.stop();
stopAllTimers();
setIsPlaying(false);
setIsMuted(false);
setTimer(duration);
currentAudioUrl.current = null;
} catch (error) {
ErrorHandler(error);
}
}, [stopAllTimers, duration]);
const startTimerSync = useCallback(() => {
stopAllTimers();
activateKeepAwake();
syncIntervalRef.current = BackgroundTimer.setInterval(async () => {
try {
const info = await SoundPlayer.getInfo();
const currentTime = info.currentTime;
let remainingTime = Math.max(0, duration - currentTime);
setTimer(Math.round(remainingTime));
console.log('Audio Stuck, restarting playback', currentTime);
// console.log(
// 'Remaining Time:',
// remainingTime,
// 'Rounded:',
// Math.round(remainingTime),
// 'currentTime:',
// currentTime,
// 'duration:',
// info.duration,
// );
if (Math.round(remainingTime) === 2 && beepCount?.current === 0) {
beepCount.current += 1;
console.log('Beep Sound Triggered');
routineBeepSound();
}
if (
Math.round(remainingTime) === 0 &&
beepCount?.current === 1 &&
!isTimerCompleted
// ||
// (audioCompleted?.current && !isTimerCompleted)
) {
console.log('Timer Completed');
beepCount.current += 1;
setIsTimerCompleted(true);
SoundPlayer.stop();
stopAllTimers();
}
} catch (e) {}
}, 500);
}, [duration, isTimerCompleted, routineBeepSound, stopAllTimers]);
const startSimpleCountdown = useCallback(() => {
stopAllTimers();
activateKeepAwake();
countdownIntervalRef.current = BackgroundTimer.setInterval(() => {
setTimer(prev => {
if (prev <= 1) {
stopAllTimers();
setIsTimerCompleted(true);
return 0;
}
if (prev === 3) {
routineBeepSound();
}
return prev - 1;
});
}, timerInterval);
}, [stopAllTimers, timerInterval, routineBeepSound]);
const loadSound = useCallback(async (url: string): Promise => {
if (url && url !== 'null' && currentAudioUrl.current !== url) {
try {
await SoundPlayer.loadUrl(url);
currentAudioUrl.current = url;
return true;
} catch (error) {
ErrorHandler(error);
currentAudioUrl.current = null;
return false;
}
}
if (currentAudioUrl.current === url && url) {
return true;
}
currentAudioUrl.current = null;
return false;
}, []);
const playSound = useCallback(async () => {
setIsPlaying(true);
if (currentAudioUrl.current) {
// try {
// SoundPlayer.play();
// SoundPlayer.setVolume(isMuted ? 0 : 1);
// startTimerSync();
// } catch (error) {
// ErrorHandler(error);
// }
try {
if (!audioLoaded.current) {
await new Promise(resolve => {
const sub = SoundPlayer.addEventListener(
'FinishedLoadingURL',
() => {
audioLoaded.current = true;
if (!audioLoadedMounted.current) {
return;
}
sub.remove();
resolve();
},
);
});
}
if (!audioLoadedMounted.current) {
return;
}
setIsPlaying(true);
await SoundPlayer.play();
SoundPlayer.setVolume(isMuted ? 0 : 1);
// await SoundPlayer.seek(0);
const info = await SoundPlayer.getInfo();
const remainingTime = Math.max(0, duration - info.currentTime);
setTimer(Math.round(remainingTime));
startTimerSync();
} catch (error) {
// ErrorHandler(error);
}
} else {
startSimpleCountdown();
}
}, [isMuted, duration, startTimerSync, startSimpleCountdown]);
const pauseSound = useCallback(() => {
setIsPlaying(false);
stopAllTimers();
try {
SoundPlayer.pause();
} catch (error) {}
}, [stopAllTimers]);
const startTimer = playSound;
const stopTimer = pauseSound;
const seekToProgress = useCallback(
async (progress: number) => {
if (!duration || !currentAudioUrl.current) {
return;
}
const clampedProgress = Math.max(0, Math.min(1, progress));
const seekPosition = duration * clampedProgress;
const remainingTime = duration - seekPosition;
try {
await SoundPlayer.seek(seekPosition);
setTimer(Math.ceil(remainingTime));
if (isPlaying) {
SoundPlayer.play();
}
if (beepSound.current?.play) {
beepCount.current = 0;
beepSound?.current?.stop();
}
} catch (error) {
ErrorHandler(error);
}
},
[duration, isPlaying],
);
const seekFifteenSec = useCallback(
async (offset: number) => {
if (!duration || !currentAudioUrl.current) {
return;
}
if (offset === -15) {
beepCount.current = 0;
beepSound.current?.stop();
}
try {
const info = await SoundPlayer.getInfo();
const seekTo = Math.min(
Math.max(0, info.currentTime + offset),
duration,
);
await SoundPlayer.seek(seekTo);
// if (isPlaying) {
// SoundPlayer.play();
// }
const remaining = Math.max(0, duration - seekTo);
setTimer(Math.ceil(remaining));
} catch (error) {
ErrorHandler(error);
}
},
[duration],
);
const muteUnmute = useCallback(() => {
setIsMuted(prev => {
const isNowMuted = !prev;
const newVolume = isNowMuted ? 0 : 1;
SoundPlayer.setVolume(newVolume);
beepSound.current?.setVolume(newVolume);
return isNowMuted;
});
}, []);
const playPause = useCallback(
(val: boolean) => {
setIsPause(val);
if (val) {
pauseSound();
} else {
playSound();
}
},
[pauseSound, playSound],
);
useEffect(() => {
const onAppStateChange = (nextAppState: AppStateStatus) => {
if (
appState.current.match(/active/) &&
nextAppState.match(/inactive|background/)
) {
if (isPlaying) {
pauseSound();
setIsPause(true);
}
}
appState.current = nextAppState;
};
if (isFocused) {
const sub = AppState.addEventListener('change', onAppStateChange);
return () => sub.remove();
}
}, [pauseSound, isFocused, isPlaying]);
useEffect(() => {
if (isFocused) {
audioLoadedMounted.current = true;
return () => {
audioLoadedMounted.current = false;
};
}
}, [isFocused]);
useEffect(() => {
if (isFocused) {
return () => {
stopSound();
if (beepSound.current) {
beepSound.current.release();
beepSound.current = null;
beepCount.current = 0;
}
setTimer(duration ?? 0);
setIsTimerCompleted(false);
if (audioLoaded?.current) {
audioLoaded.current = false;
}
};
}
}, [isFocused, duration, stopSound]);
useEffect(() => {
if (!isFocused) {
return;
}
const onFinishedPlaying = () => {
console.log('[Audio Event] Finished Playing');
audioCompleted.current = true;
};
const onAudioLoaded = () => {
console.log('[Audio Event] Audio Loaded');
audioLoaded.current = true;
};
const finishedSub = SoundPlayer.addEventListener(
'FinishedPlaying',
onFinishedPlaying,
);
const loadedSub = SoundPlayer.addEventListener(
'FinishedLoadingURL',
onAudioLoaded,
);
return () => {
finishedSub.remove();
loadedSub.remove();
};
}, [isFocused]);
return {
isPlaying,
timer,
isTimerCompleted,
isMuted,
startTimer,
stopTimer,
loadSound,
playSound,
pauseSound,
stopSound,
muteUnmute,
routineCompleteSound,
isPause,
playPause,
initializeSounds,
seekToProgress,
seekFifteenSec,
};
};