I am having trouble cancelling loadAsync when a user navigates away from my page. I have tried to use a cleanup function on useEffect but since the soundObject hasn't loaded yet it will give me an error since soundObject equals null. I have also tried to use redux and add soundObject.stopAsync when other pages come into focus but since the soundObject may not be set yet it will not cancel and I will have audio playing and can't be stopped. Here is my Pause/Play button component where I call loadAsync. Any help would be greatly appreciated. Thanks
UPDATE TO MY PLAY PAUSE HANDLER I have found a workaround even though I feel there is a better way. I am now calling Audio.setIsEnabledAsync(false); as a cleanup function.
//CLEANUP FUNCTION
useEffect(() => {
Audio.setIsEnabledAsync(true);
return function cleanUp() {
reference.putFile(props.audioFile).cancel();
Audio.setIsEnabledAsync(false);
};
}, []);
import React, { useState, useEffect } from "react";
import { TouchableOpacity } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import storage from "@react-native-firebase/storage";
import { playPause, stopPlay } from "../../../store/actions/playerActions";
import { Audio } from "expo-av";
import SmallIndicator from "../Indicators/SmallIndicator";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
import { faPlay, faPause } from "@fortawesome/pro-light-svg-icons";
import Colors from "../../../constants/Colors";
const PlayPause = (props) => {
const dispatch = useDispatch();
// LOAD FROM FIREBASE VARIABLES
let audioFile = props.audioFile;
const reference = storage().ref(audioFile);
let task = reference.getDownloadURL();
//HOOKS
const isPlaying = useSelector((state) => state.player.isPlaying);
const [iconSwitch, setIconSwitch] = useState(faPlay);
const [soundObject, setSoundObject] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// LOAD AUDIO SETTINGS
useEffect(() => {
const audioSettings = async () => {
try {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
shouldDuckAndroid: true,
staysActiveInBackground: false,
playThroughEarpieceAndroid: true,
});
} catch (e) {
console.log(e);
}
audioSettings();
};
}, []);
//CLEANUP FUNCTION
useEffect(() => {
Audio.setIsEnabledAsync(true);
return function cleanUp() {
reference.putFile(props.audioFile).cancel();
Audio.setIsEnabledAsync(false);
};
}, []);
// STOP PLAY ON PAGE EXIT
useEffect(() => {
ifPlaying();
}, [isPlaying]);
const ifPlaying = async () => {
if (isPlaying === false && soundObject != null) {
await soundObject.stopAsync();
await soundObject.unloadAsync();
setSoundObject(null);
setIconSwitch(faPlay);
}
};
// PLAY PAUSE TOGGLE
const handlePlayPause = async () => {
setIsLoading(true);
let uri = await task;
//PLAY
if (isPlaying === false && soundObject === null) {
const soundObject = new Audio.Sound();
await soundObject.loadAsync({ uri }, isPlaying, true);
setSoundObject(soundObject);
soundObject.playAsync();
dispatch(playPause(true));
setIconSwitch(faPause);
// PAUSE
} else if (isPlaying === true && soundObject != null) {
dispatch(playPause(false));
setIconSwitch(faPlay);
// STOP AND PLAY
} else if (isPlaying === true && soundObject === null) {
dispatch(stopPlay(true));
dispatch(playPause(true));
const soundObject = new Audio.Sound();
const status = { shouldPlay: true };
await soundObject.loadAsync({ uri }, status, true);
setSoundObject(soundObject);
soundObject.playAsync();
setIconSwitch(faPause);
// RESUME PLAY
} else if (isPlaying === false && soundObject != null) {
dispatch(playPause(true));
soundObject.playAsync();
setIconSwitch(faPause);
}
setIsLoading(false);
};
console.log(isPlaying);
if (isLoading) {
return <SmallIndicator />;
}
return (
<TouchableOpacity onPress={handlePlayPause}>
<FontAwesomeIcon icon={iconSwitch} size={35} color={Colors.primary} />
</TouchableOpacity>
);
};
export default PlayPause;
The PlayPause Component is located in my SongItem Component, I wont add the code that isn't applicable.
const SongItem = (props) => {
return (
<View>
<PurchaseModal
visible={modalToggle}
purchaseSelector={purchaseSelector}
radio_props={LicenseData}
onPress={modalToggleHandler}
/>
<View>
<Card>
<BodyText>{props.items.name}</BodyText>
<View style={styles.innerContainer}>
<PlayPause audioFile={props.items.audio} />
<TouchableOpacity onPress={cartPress}>
<FontAwesomeIcon
icon={iconSwitch}
size={35}
color={Colors.primary}
/>
</TouchableOpacity>
</View>
<TouchableOpacity onPress={modalToggleHandler} style={toggleStyle}>
<FontAwesomeIcon
icon={faFileInvoice}
size={35}
color={Colors.primary}
/>
</TouchableOpacity>
</Card>
</View>
</View>
);
};
The SongItem is located on my SongScreen. when I call dispatch(stopPlay) I am switching isPlaying to false;
const SongScreen = (props) => {
const filteredSongs = useSelector((state) => state.filter.filteredSongs);
const { goBack } = props.navigation;
const dispatch = useDispatch();
const backPress = () => {
dispatch(stopPlay());
goBack();
};
useEffect(() => {
props.navigation.addListener("didBlur", () => {
dispatch(stopPlay());
});
});
return (
<Gradient>
<FlatList
removeClippedSubviews={false}
windowSize={2}
maxToRenderPerBatch={6}
data={filteredSongs}
keyExtractor={(item) => item.id.toString()}
renderItem={(itemData) => <SongItem items={itemData.item} />}
/>
<MainButton name={"Back"} onPress={backPress} />
</Gradient>
);
}
};
I have struggled with the same challenge even asked expo directly and checked their source code, to find out there is no way to cancel and already loading Audio.
What i did was solve it with the help of
setOnPlaybackStatusUpdate
callback.Explaining: if i want to cancel a song first i have to wait until it has loaded and with the help of
setOnPlaybackStatusUpdate
it's possible to immediately stop and unload audio after it finishes loading.so your
stopPlay
function for me would look like this:The StopListener would look like this
Please share if you have found any other solution.