Tyler Joe
Tyler Joe

Reputation: 377

Expo stop audio playing after component closed

I'm using expo's audio API for playing sounds. I have a component inside a react-native-raw-bottom-sheet (Its a pop from the bottom of the screen), where I have the audio logics.

When I close the popup I want the audio to stop playing. I tried using a cleanup function in the audio component, but I got an error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Main component:

<RBSheet
  //props
  onClose({() =>{
  //maybe do something here
 }}
 >
  <Audio /> //this is the adudio component
<RBSheet>

Audio component:

 const [soundStatus, setSoundStatus] = useState({ status: null, icon: play });
 const [audioStatus, setAudioStatus] = useState(0);

 useEffect(() => {
    let mounted = true;

    if (soundStatus.status) {
      if (soundStatus.status.isLoaded) {
        sound.stopAsync().then(() => {
          if (mounted) {
            setSoundStatus({ status: st, icon: play });
            setAudioFinished(false);
          }
        });
      }
    }
    return () => {
      mounted = false;
    };
  }, []);

Upvotes: 0

Views: 1885

Answers (3)

Four
Four

Reputation: 1084

This is from 'expo-av' official document.

import * as React from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
import { Audio } from 'expo-av';

export default function App() {
  const [sound, setSound] = React.useState();

  async function playSound() {
    console.log('Loading Sound');
    const { sound } = await Audio.Sound.createAsync(
       require('./assets/Hello.mp3')
    );
    setSound(sound);

    console.log('Playing Sound');
    await sound.playAsync(); }

// This part is what you are looking for
  React.useEffect(() => {
    return sound
      ? () => {
          console.log('Unloading Sound');
          sound.unloadAsync(); }
      : undefined;
  }, [sound]);

Upvotes: 1

Tyler Joe
Tyler Joe

Reputation: 377

I solved the problem. I had two probelms. The first one was that in the onPlaybackStatusUpdate function I tried to update a <Text> with the remaining audio status when the component was already unmounted so It gave an error. I fixed this with adding if(mountedRef.current) in the onPlaybackStatusUpdate function.

The second problem I had that I tried to run the cleanup function in an empty useEffect dependency array ( useEffect(() => {},[])). The cleanup function ran, but the sound was null, so It couldn't stop the sound. I fixed it by adding the sound state in the useEffect dependency array, and checking if the mountedRef was true (described below).

 const mountedRef = useRef(null)
 
 //Tracking audio status changes

 //Fix of problem 1
  const onPlaybackStatusUpdate = async (st) => {
    //This is the fucntion which updates the elapsed audio status ( shows remaining seconds like this 0:10/0:53)
    if(mountedRef.current) 
      //set some state here for updating the audio status
  };

  //Loading the file for the first time
  useEffect(() => {
    (async () => {
      if (sound === null) {
      if(audioUrl) {
        try {
            const { sound, status } = await Audio.Sound.createAsync(
              {
                uri: `audioUrl`,
              },
              { shouldPlay: false },
              onPlaybackStatusUpdate //Problem was here
            );

            setSound(sound);
            setSoundStatus({ icon: play, status: status });

            console.log("setting sound");
          } catch (err) {
            console.log(err);
            setSoundStatus({ icon: play, status: null });
          }
      }    
      }
    })();
  }, [audioUrl]);

  //Fix of problem 2
  useEffect(() => {
    mountedRef.current = true;

    return () => {
      stopMusic();
      mountedRef.current = false;
    };
  }, [sound]);

  const stopMusic = async () => {
    if (sound) {
      let st = await sound.stopAsync();
      if (mountedRef.current) {
        setSoundStatus({ status: st, icon: play });
        setAudioFinished(true);
      }
    }
  };

Upvotes: 1

Kartikey
Kartikey

Reputation: 4992

What you can do is in the MainComponent create a State Variable called AudioPlaying and manage the audio from that variable. Let me show you

add these lines to Main Component

...

const [AudioPlaying, SetAudioPlaying] = useState(false)

<RBSheet
  //props
  onOpen={() => SetAudioPlaying(true)} // Setting Playing to true when Sheet opens
  onClose={() => SetAudioPlaying(false)} // Setting Playing to false when Sheet closes
 >
  <Audio playing={AudioPlaying} /> // Pass the variable as a Prop here
<RBSheet>

...

Then in Audio component

// All Imports etc

function Audio({ playing }) {
  //Rest of your code
  const [soundStatus, setSoundStatus] = useState({ status: null, icon: play });
  const [audioStatus, setAudioStatus] = useState(0);

 useEffect(() => {
    // This useEffect runs whenever there is a change in playing Prop
    if (soundStatus.status) {
      if (soundStatus.status.isLoaded) {
        sound.stopAsync().then(() => {
          if (playing === true) {
            setSoundStatus({ status: st, icon: play });
            setAudioFinished(false);
          } else {
            // Pause your Audio here
          }
        });
      }
    }
  }, [playing]);

  //Rest of your code
}

export default Audio;

Upvotes: 1

Related Questions