zmxv / react-native-sound

React Native module for playing sound clips
MIT License
2.78k stars 747 forks source link

Audio plays as soon as an instance is created #819

Open n-ii-ma opened 1 year ago

n-ii-ma commented 1 year ago

This packages has an annoying bug which makes it totally useless and that is playing without even calling the .play() method.

This only happens on Android. The issue is the fact that the method is being called inside of the callback of Sound:

var whoosh = new Sound('whoosh.mp3', Sound.MAIN_BUNDLE, (error) => {
  if (error) {
    console.log('failed to load the sound', error);
    return;
  }
  // loaded successfully
  console.log('duration in seconds: ' + whoosh.getDuration() + 'number of channels: ' + whoosh.getNumberOfChannels());

  // Play the sound with an onEnd callback
  whoosh.play((success) => {
    if (success) {
      console.log('successfully finished playing');
    } else {
      console.log('playback failed due to audio decoding errors');
    }
  });
});

And if the method of .play() isn't called inside of it, it won't work at all.

sultson commented 1 year ago

Hey, @n-ii-ma, does this example fix the issue?

App.tsx

import React from 'react';
import { View, Button, StyleSheet } from 'react-native';
import Sound from 'react-native-sound'

Sound.setCategory('Playback');
var whoosh = new Sound('whoosh.mp3', Sound.MAIN_BUNDLE, (error) => {
  if (error) {
    console.log('failed to load the sound', error);
    return;
  }
});

function App(): JSX.Element {

  function playSound() {
    whoosh.play((success) => {
      if (!success) { console.log('Playback failed due to decoding errors') }
    });
  }

  return (
    <View style={styles.container}>
     <Button title='Play Sound' onPress={playSound} />

    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent:'center',
    alignItems:'center'
  }
})

export default App;

Environment

package version
react-native 0.71.3
react-native-sound 0.11.2
n-ii-ma commented 1 year ago

Hey, @n-ii-ma, does this example fix the issue?

App.tsx

import React from 'react';
import { View, Button, StyleSheet } from 'react-native';
import Sound from 'react-native-sound'

Sound.setCategory('Playback');
var whoosh = new Sound('whoosh.mp3', Sound.MAIN_BUNDLE, (error) => {
  if (error) {
    console.log('failed to load the sound', error);
    return;
  }
});

function App(): JSX.Element {

  function playSound() {
    whoosh.play((success) => {
      if (!success) { console.log('Playback failed due to decoding errors') }
    });
  }

  return (
    <View style={styles.container}>
     <Button title='Play Sound' onPress={playSound} />

    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent:'center',
    alignItems:'center'
  }
})

export default App;

Environment

package version react-native 0.71.3 react-native-sound 0.11.2

On iOS it does work in this way but on Android I have to call the .play() method inside the callback of creating the Sound instance for it to work and that results in it playing as soon as it's created.

sultson commented 1 year ago

Hmm that is weird, it works on my Android device. What can you see in the Android Studio logcat when pressing the button to play the sound? (without having the .play() inside the callback)

I get those 2 lines everytime the audio is played: image

n-ii-ma commented 1 year ago

At first it logs the same lines as you but then logs these: Screenshot 2023-02-26 at 1 12 10 PM

Might be because I'm calling the .play method inside a useEffect since by doing a button press it does actually play.

n-ii-ma commented 1 year ago

I just tried calling the .play() method with a button and still nothing. It seems that the asset won't get played at first but then it will.

Getting a lot of headaches with this library, TBH.

sultson commented 1 year ago

You can solve it using the useState() hook, this way, you can display a loading screen until the asset has been loaded properly & then dismiss it. Also, wrap the function with useMemo() so that it will only be executed on the first render, like this:

const [loading, setLoading] = useState(true)
const whoosh =  useMemo(() => {
  return ( 
    new Sound('whoosh.mp3', Sound.MAIN_BUNDLE, (error) => {
      if (error) {
        console.log('failed to load the sound', error);
        return;
      }
      setLoading(false)
    })
  )
},[]);
n-ii-ma commented 1 year ago

I did as you mentioned and still nothing but I just realized that I'm releasing the audio in the cleanup function of useEffect and that seems to have been the issue here since the cleanup function runs when the conditions set in useEffect are not met and this causes the audio not to play. This is how my useEffect looks at the moment:

const [loading, setLoading] = useState(true); // Sound loading state

const orderMusicAudio = useMemo(() => {
    return new Sound('order_music.mp3', Sound.MAIN_BUNDLE, error => {
      // Return if there's an error
      if (error) {
        return;
      }
      // Set indefinite loop
      orderMusicAudio.setNumberOfLoops(-1);

      // Set loading to false
      setLoading(false);
    });
  }, []);

// Play sound if an order has been selected
  useEffect(() => {
    // Play sound if sound has loaded, the courier has incoming orders and hasn't yet accepted any
    if (!loading && hasOrders && isObjEmpty(acceptedOrder)) {
      // Start vibration
      Vibration.vibrate([1000, 1000], true);

      // Play the audio file
      // After it's completed, stop the vibration and release the audio player resource
      orderMusicAudio.play(() => {
        Vibration.cancel();
        orderMusicAudio.release();
      });
    }
    // Stop playing the audio file if otherwise
    else {
      // After the audio is stopped, stop the vibration and release the audio player resource
      orderMusicAudio.stop(() => {
        Vibration.cancel();
        orderMusicAudio.release();
      });
    }

    // Release the audio player resource on unmount
    return () => {
      console.log('RELEASE');
      orderMusicAudio.release();
    };
  }, [loading, hasOrders, acceptedOrder]);

This is for an app like Uber for the drivers which will play a sound after an order arrives via a Socket Connection. (The reason I'm releasing the audio after each successful play and stop is for performance purposes although to be honest, I'm beginning to doubt its usefulness)

I myself am a bit confused here for even if the cleanup function gets called, after the conditions are met, the .play() method is invoked and the sound should play again but it doesn't.

Also, as I mentioned before, this problem only occurs on Android.

sultson commented 1 year ago

You might be over-complicating things with .release(), try this instead:

useEffect(() => {
  if(loading) return 

  if (hasOrders && isObjEmpty(acceptedOrder)) {
    Vibration.vibrate([1000, 1000], true);
    orderMusicAudio.play((success) => {
      if (!success) { console.log('Playback failed due to decoding errors') }
    });
  } else {
    orderMusicAudio.stop()
  }

},[loading, hasOrders, acceptedOrder])

Let me know if it works now!

n-ii-ma commented 1 year ago

Yeah this does work indeed but since no cleanup is done in the useEffect, closing the app while the audio is still playing doesn't release the resource and stop the audio.

One workaround I found was to just stop playing the sound inside the cleanup function yet I'm wondering if not releasing the audio might lead to memory leak issues.

BraveEvidence commented 1 year ago

This will help https://www.youtube.com/watch?v=vVI7ZAZq5e0