Search code examples
javascriptreactjsreact-nativeexpoes6-promise

How to run async functions sequentially on a React-Native app, using Expo



UPDATE

Combining both solutions below, I wrote:

const startMusic = async () => {
    let currentSong
    let songPath
    const songArray = [
      { path: require("../assets/sounds/Katsu.mp3"), song: mainTheme },
      { path: require("../assets/sounds/MainTheme2.mp3"), song: mainTheme2 },
      { path: require("../assets/sounds/MainTheme3.mp3"), song: mainTheme3 },
    ]

    for (var i = 0; i < songArray.length; i++) {
      currentSong = songArray[i].song
      songPath = songArray[i].path
      try {
        await currentSong.loadAsync(songPath)
        await currentSong.playAsync()
        // setSoundObject(currentSong)
        console.log("Music will start")
        return new Promise(resolve => {
          currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
            if (playbackStatus.didJustFinish) {
              console.log("Just finished playing")
              resolve()
            }
          })
        })
      } catch (error) {
        console.log(`Error: ${error}`)
        return
      }
    }
  }

This actually plays the song, and the console logs occur on time ("Just finished playing" happens exactly when the song ends) I'm trying to figure out how to play the next song.. and how will it know when it has reached the final song?

return new Promise(resolve => {
  currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
    if (playbackStatus.didJustFinish) {
      console.log("Just finished playing")
      resolve()
    }
  })
}).then(() => console.log("Next song?"))

Figured how where to put the .then to get it to console log right after "Just finished playing" I'm just trying to see how to actually put the next song there (then of course, telling it when to go back to the first song in the array)


Original Post

Working on an assignment for a react native app using expo-av library for Sound files. Right now, the app has a startMusic function set in a Context file that is responsible for playing the app's background music. It only has one song for now:

const startMusic = async () => {
    try {
      await mainTheme.loadAsync(require("../assets/sounds/Katsu.mp3"))
      await mainTheme.playAsync()
      setSoundObject(mainTheme)
      console.log("The first song is playing! Enjoy!")
    } catch (error) {
      console.log(`Couldnt load main theme: ${error}`)
      return
    }
  }

It is used in the homescreen component's file like so:

const { startMusic } = useContext(MusicContext)

useEffect(() => {
  startMusic()
}, [])

For the second song, I wrote another const in the MusicContext file:

const secondSong = async () => {
    try {
      await mainTheme2.loadAsync(require("../assets/sounds/MainTheme2.mp3"))
      await mainTheme2.playAsync()
      setSoundObject(mainTheme2)
      console.log("Now playing the second track. Enjoy!")
    } catch (error) {
      console.log(`Could not play the second song: ${error}`)
      return
    }
  }

Annnnnd… here is where my trouble lies. I know this wasn't gonna work but I wrote this in the component file to try to get the second song playing after the first song

useEffect(() => {
    startMusic()
      .then(secondSong())
  }, [])

I know there's more to it than that but I'm having trouble.


Solution

  • Problem with your code is not just running one function after another (that would be as simple as startMusic().then(() => secondSong()) but still won't solve the problem), but the fact that your functions actually don't wait for a song to finish playing before resolving

    You expect this line await mainTheme.playAsync() to pause function execution until the song has finished, but what it in fact does according to docs https://docs.expo.io/versions/latest/sdk/av/ is exactly only starting the playback (without waiting for it to finish)

    With that being said, you need to determine the moment your playback finishes, then create a Promise that will only resolve after the playback is finished so that your second song can only start after the first

    In the simplest form without error handling and such, it can look like this

    const startAndWaitForCompletion = async () => {
      try {
        await mainTheme.loadAsync(require('../assets/sounds/Katsu.mp3'))
        await mainTheme.playAsync()
        console.log('will start playing soon')
        return new Promise((resolve) => {
          mainTheme.setOnPlaybackStatusUpdate(playbackStatus => {
            if (playbackStatus.didJustFinish) {
              console.log('finished playing')
              resolve()
            }
          }
        })
      } catch (error) {
        console.log('error', error)
      }
    }
    

    the trick is of course the .setOnPlaybackStatusUpdate listener that will be called every so often with playback status, and by analyzing the status you can tell the song has finished playing. If you scroll to the bottom of the page I linked you will find other examples with status update


    updated

    const startAndWaitForCompletion = async (playbackObject, file) => {
      try {
        await playbackObject.loadAsync(file)
        await playbackObject.playAsync()
        console.log('will start playing soon')
        return new Promise((resolve) => {
          playbackObject.setOnPlaybackStatusUpdate(playbackStatus => {
            if (playbackStatus.didJustFinish) {
              console.log('finished playing')
              resolve()
            }
          }
        })
      } catch (error) {
        console.log('error', error)
      }
    }
    
    ////
    
    const songs = [
      { path: require('../assets/sounds/Katsu.mp3'), song: mainTheme },
      { path: require('../assets/sounds/MainTheme2.mp3'), song: mainTheme2 },
      { path: require('../assets/sounds/MainTheme3.mp3'), song: mainTheme3 },
    ]
    
    
    useEffect(() => {
      (async () => {
        for (let i = 0; i < songs.length; i++) {
          await startAndWaitForCompletion(songs[i].song, songs[i].path)
        }
      })()
    }, [])