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