Skip to content

iOS: Audio currentTime freezes after seeking near end, shows 5–20s remaining #181

@satyamn2024

Description

@satyamn2024

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,
};
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions