1

This is my first time working with React Native together with Expo and I have been learning a lot, but this is something I am still unable to fix.

I am using expo-av and for short sounds I am not having any problem. They play and then I call unloadAsync() and no issues there. But when it comes to the background music, I am stopping, unloadind, making sure the useEffect is returning but for some reason even after the game is over and I am sent to the game over or victory screen, the music keeps playing and if for some reason I try to start another game, it will be one audio on top of the other.

Inside the class responsible for seting up playback:

 class AudioController {

    constructor() {

        this.bgMusic = new Audio.Sound()
        this.flipSound = new Audio.Sound()
        this.matchSound = new Audio.Sound()
        this.victorySound = new Audio.Sound()
        this.gameOverSound = new Audio.Sound()
    }

    
    loadAndPlay = async (audioObject, audioFile, loop = false, volume = 1.0) => {

        try {
            await audioObject.loadAsync(audioFile);
            audioObject.setIsLoopingAsync(loop);
            audioObject.setVolumeAsync(volume);
            await audioObject
                .playAsync()
                .then(async playbackStatus => {

                    if (!loop) {
                        setTimeout(() => {
                            audioObject.unloadAsync()
                        }, playbackStatus.playableDurationMillis)
                    }
                })
                .catch(error => {
                    console.log(error)
                })

        } catch (error) {
            console.log(error);
        }
    }


    playBgMusic = async () => {
        await this.loadAndPlay(this.bgMusic, bgMusic, true, 0.5); // Loop and set the volume to 50%
    }


    stopBgMusic = async () => {
        try {

            await this.bgMusic.stopAsync();
            await this.bgMusic.unloadAsync();

        } catch (error) {
            console.log(error);
        }
    }

    playFlipSound = async () => {
        await this.loadAndPlay(this.flipSound, flipSound);
    }

    playMatchSound = async () => {
        await this.loadAndPlay(this.matchSound, matchSound);
    }

    playVictorySound = async () => {
        await this.loadAndPlay(this.victorySound, victorySound);
    }

    playGameOverSound = async () => {
        await this.loadAndPlay(this.gameOverSound, gameOverSound);
    }
}


export default AudioController``



Inside the component where I want to use it.

`useEffect(() => {

    audioController.playBgMusic()

    resetTurn()
    setTimer(level)
    shuffleCards()

    return () => {

       console.log(' Start game cleanup executed')

audioController.stopBgMusic()
}
}, []);`

Of course I am properly instancing the audioController (or the other sounds would not be working) and I am triggering the game over and victory functions. So the problem is either with the way I am trying to stop the music or the component is not unmounting. I have also tried to stop the music inside the fuction that handles the game over right before it sends me to a different screen and still the same. Also tried using React Context and the result is the same, the music will not stop and new instances will start if I try to start a new game.

Tiger
  • 53
  • 9

1 Answers1

1

react native navigation lifecycle doesnt work like react you need to listen to focus and blur event to detect if the component is hidded or shown just like this :

  useEffect(() => {
    const subscribe = navigation.addListener('focus', () => {
      // Screen was focused
      // Do something
      console.log('play sound');

      controller.playBackgroundMusic();
    });

    const unsubscribe = navigation.addListener('blur', () => {
      // Screen was blurred
      // Do something
      console.log(' Start game cleanup executed');
      controller.stopBackgroundMusic();
    });

    return unsubscribe;
  }, []);

you can learn more at :

https://reactnavigation.org/docs/navigation-lifecycle/

This is a small demo how to play and stop background music using native react :

SandBox : https://snack.expo.dev/EjVXFzojr

App.js

import {NavigationContainer} from '@react-navigation/native';

// or any files within the Snack
import GameScene from './components/GameScene';
import IntroScene from './components/IntroScene';
import {createNativeStackNavigator} from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Intro"
          component={IntroScene}
          options={{title: 'Intro'}}
        />
        <Stack.Screen name="Game" component={GameScene} options={{title: 'Game'}} />
      </Stack.Navigator>

    </NavigationContainer>
  );
}

./controllers/AudioController.js

import {Audio} from "expo-av"

class AudioController {
  constructor() {
    this.backgroundMusic = new Audio.Sound();
  }

  loadAndPlay = async (audioObject, audioFile, loop = false, volume = 1.0) => {
    try {
      await audioObject.loadAsync(audioFile);
      audioObject.setIsLoopingAsync(loop);
      audioObject.setVolumeAsync(volume);
      await audioObject
        .playAsync()
        .then(async (playbackStatus) => {
          if (!loop) {
            setTimeout(() => {
              audioObject.unloadAsync();
            }, playbackStatus.playableDurationMillis);
          }
        })
        .catch((error) => {
          console.log(error);
        });
    } catch (error) {
      console.log(error);
    }
  };

  playBackgroundMusic = async () => {

    await this.loadAndPlay(
      this.backgroundMusic,
      require('../assets/FitGirl-Repacks.mp3'),
      true,
      1
    ); // Loop and set the volume to 50%
  };

  stopBackgroundMusic = async () => {
    try {
      await this.backgroundMusic.stopAsync();
      await this.backgroundMusic.unloadAsync();
    } catch (error) {
      console.log(error);
    }
  };
}
export function audioController(){
  return new AudioController()
}

./components/IntroScene.js

import React, { useState, useEffect } from 'react';
import { Text, View, StyleSheet, Image, Button } from 'react-native';
import { audioController } from '../controllers/AudioController';

export default function IntroScene({ navigation }) {
  const startHandler = (event) => {
    navigation.navigate('Game');
  };

  const controller = audioController();

  useEffect(() => {
    const subscribe = navigation.addListener('focus', () => {
      // Screen was focused
      // Do something
      console.log('play sound');

      controller.playBackgroundMusic();
    });

    const unsubscribe = navigation.addListener('blur', () => {
      // Screen was focused
      // Do something
      console.log(' Start game cleanup executed');
      controller.stopBackgroundMusic();
    });

    return unsubscribe;
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>Intro Game</Text>
      <Button title="start" onPress={(event) => startHandler(event)} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  paragraph: {
    margin: 24,
    marginTop: 0,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
});

./components/GameScene.js

import { Text, View, StyleSheet, Image } from 'react-native';

export default function GameScene() {
  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>
        Play The Game
      </Text>
      <Image style={styles.logo} source={require('../assets/snack-icon.png')} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  paragraph: {
    margin: 24,
    marginTop: 0,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  logo: {
    height: 128,
    width: 128,
  }
});
Christa
  • 398
  • 7
  • I actually figured out how to do this on my own after trying a few more things but I did use the focus and blur to achieve it, so your answer is correct and very helpful. – Elisabete Costa Aug 26 '23 at 17:06