2

I'm extending the functionality of a timer component to add a beep sound for the last 3 seconds of the timer. That works fine, the issue is after all is said and done. I'm doing the following:

  1. Wrapping the timer component on a new functional component
  2. Initialise the Audio.Sound() in the body of the component
  3. Using useEffect to load the sound initially
  4. On each timer event I check if I should play the sound and play it using replayAsync
  5. On the useEffect cleanup I unload the sound object with unloadAsync

A second or so after the last beep has played and I've navigated to the following screen, I get an error which I'll past below in full. It seems there's a seeking operation being called on my sound object by the expo-av library, but my component is no longer there:

[Unhandled promise rejection: Error: Seeking interrupted.]

I've tried the following with no success:

  • Make the calls to loadAsync and unloadAsync await calls
  • Tried to setOnPlaybakStatusUpdate to null to try and prevent statusUpdate calls
  • I've even tried to not unload the soudn via unloadAsync

My code is:

import React from 'react'
import CountDown from 'react-native-countdown-component'
import { Audio } from 'expo-av'
const BEEP_START = 3

const CountDownBeep = (props) => {
  console.log('Sound Created')
  const beepSound = new Audio.Sound()

  React.useEffect(() => {
      async function loadSound() {
          console.log("Sound Initialized")
          await beepSound.loadAsync(require('../assets/sounds/beep.wav'), {
              shouldPlay: false,
              isLooping: false,
          })
          // This is not by design, just one of my attempts to get rid of the error
          beepSound.setOnPlaybackStatusUpdate()
      }

      loadSound()

      // Cleanup, tried with async and without
      return async () => {
          console.log('Sound destroyed')
          await beepSound.unloadAsync()
      }
  })

  const countDownTimerChangedHandler = (timeLeft) => {

      // This works fine
      if (timeLeft <= BEEP_START + 1 && timeLeft > 0) {
          console.log('Sound Played:', timeLeft)
          beepSound.replayAsync()
      }
  }

  return (
      <CountDown
          {...props}
          onChange={(timeLeft) => countDownTimerChangedHandler(timeLeft)}
      />
  )
}

export default CountDownBeep

The functionality is 100%, but after a few ms or a second or two after I navigate away to the next screen I get the following error:

[Unhandled promise rejection: Error: Seeking interrupted.]
- node_modules/react-native/Libraries/BatchedBridge/NativeModules.js:103:50 in promiseMethodWrapper
- node_modules/@unimodules/react-native-adapter/build/NativeModulesProxy.native.js:15:23 in moduleName.methodInfo.name
- node_modules/expo-av/build/Audio/Sound.js:138:24 in replayAsync
- node_modules/expo-av/build/Audio/Sound.js:5:33 in <anonymous>
- node_modules/regenerator-runtime/runtime.js:63:36 in tryCatch
- node_modules/regenerator-runtime/runtime.js:293:29 in invoke
- node_modules/regenerator-runtime/runtime.js:63:36 in tryCatch
- node_modules/regenerator-runtime/runtime.js:154:27 in invoke
- node_modules/regenerator-runtime/runtime.js:189:16 in PromiseImpl$argument_0
- node_modules/promise/setimmediate/core.js:45:6 in tryCallTwo
- node_modules/promise/setimmediate/core.js:200:22 in doResolve
- node_modules/promise/setimmediate/core.js:66:11 in Promise
- node_modules/regenerator-runtime/runtime.js:188:15 in callInvokeWithMethodAndArg
- node_modules/regenerator-runtime/runtime.js:211:38 in enqueue
- node_modules/regenerator-runtime/runtime.js:238:8 in exports.async
- node_modules/expo-av/build/Audio/Sound.js:5:33 in <anonymous>
- node_modules/expo-av/build/Audio/Sound.js:5:33 in <anonymous>
- node_modules/regenerator-runtime/runtime.js:63:36 in tryCatch
- node_modules/regenerator-runtime/runtime.js:293:29 in invoke
- node_modules/regenerator-runtime/runtime.js:63:36 in tryCatch
- node_modules/regenerator-runtime/runtime.js:154:27 in invoke
- node_modules/regenerator-runtime/runtime.js:189:16 in PromiseImpl$argument_0
- node_modules/promise/setimmediate/core.js:45:6 in tryCallTwo
- node_modules/promise/setimmediate/core.js:200:22 in doResolve
- node_modules/promise/setimmediate/core.js:66:11 in Promise
- node_modules/regenerator-runtime/runtime.js:188:15 in callInvokeWithMethodAndArg
- node_modules/regenerator-runtime/runtime.js:211:38 in enqueue
- node_modules/regenerator-runtime/runtime.js:238:8 in exports.async
- node_modules/expo-av/build/Audio/Sound.js:5:33 in <anonymous>
* components/CountDownBeep.js:31:24 in countDownTimerChangedHandler
* components/CountDownBeep.js:39:22 in CountDown.props.onChange
- node_modules/react-native-countdown-component/index.js:115:21 in CountDown#updateTimer
- node_modules/react-native/Libraries/Core/Timers/JSTimers.js:135:14 in _callTimer
- node_modules/react-native/Libraries/Core/Timers/JSTimers.js:387:16 in callTimers
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:425:19 in __callFunction
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:112:6 in __guard$argument_0
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:373:10 in __guard
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:111:4 in callFunctionReturnFlushedQueue
* [native code]:null in callFunctionReturnFlushedQueue
bitoiu
  • 6,893
  • 5
  • 38
  • 60

2 Answers2

1

I think I've fixed it. The problem is not with the Sound API or expo-av. The issue seems to be how the timer component in question deals with callbacks. I had a suspicion, that since the next screen also has a component, that somehow the component is firing the new callback into the old component. I've added a unique ID to each instance of my component and I stopped getting errors:

 <CountDown
            {...props}
            id={new Date().toTimeString()}
            onChange={(timeLeft) => countDownTimerChangedHandler(timeLeft)}
        />

NOTE: As such I believe this question and answer are only relevant if you're using https://github.com/talalmajali/react-native-countdown-component.

Thank you for reading.

bitoiu
  • 6,893
  • 5
  • 38
  • 60
1

The other solution did not work for me. What I've tried that successfully got rid of this seeking interrupted error was is loading all of the sounds you need for a particular screen in componentDidMount. Then you can use them as you wish throughout the file.

    // This would be a function in your component class
    componentDidMount = () => {
        this.loadSounds();
   };
// You can put SoundUtils in any file!
    export const SoundUtils = {
        loadSound: async (requiredSound: AVPlaybackSource) => {
            try {
                const { sound } = await Audio.Sound.createAsync(requiredSound, {
                shouldPlay: false,
                progressUpdateIntervalMillis: 1, //It doesn't need to be this low
            });
                return sound;
            } catch (error) {
                console.log(`Sound not loaded error: ${error}`);
            }
        },
    } 
    // This would be a function in your component class
    loadsounds = () => {
        const workoutStartSound = await SoundUtils.loadSound(require("../assets/sounds/workoutStart.wav"));
        const timeToWorkoutSound = await SoundUtils.loadSound(require("../assets/sounds/timeToWork.mp3"));
    // Now to make sure you're only working with an "accepted promise"
    if (workoutStartSound !== undefined) {
      // workoutStartSound is a property of the Component class and is of type Audio.Sound
      this.workoutStartSound = workoutStartSound;
    } else {
      console.log(`Workout Start sound was not set`);
    }
    if (timeToWorkoutSound !== undefined) {
      this.timeToWorkoutSound = timeToWorkoutSound;
    } else {
      console.log(`Time to Workout sound was not set`);
    }   

Having the sounds pre-loaded will be the best way to avoid interrupting the seek!

    playSound = async () => {
        try {
            await this.workoutStartSound.playFromPositionAsync(0);
            // await this.timeToWorkoutSound.playFromPositionAsync(0);
        } catch (error) {
            console.log(`Error in start timer - ${error}`);
        }
    };
Uch
  • 94
  • 8