Search code examples
react-nativeexpobackground-musicexpo-av

Is There A Way To Add Background Music Without Adding A "Play Music" Button Using Expo AV?


I would like to add background music that plays automatically, with only maybe a "pause/stop audio"

I've tried searching within multiple sources, but all of the tutorialsthat I've found include a "play sound" music, which won't be needed.


Solution

  • They don't really show it in the examples provided on the expo-av page, but the Audio.Sound.createAsync function allows for a lot of customization. You can set the volume, decide if it should loop, decide if it should play on load, and even subscribe playback status updates and set the intervals at which you get those updates. I went overboard and fleshed out a hook to return the status, position, and duration of the song as it playbacks; and if you need to subscribe to additional info the hook will allow you to subscribe to the playback update events:

    import { useState, useEffect, useCallback, useMemo } from 'react';
    import { Audio, usePermissions } from 'expo-av';
    
    
    const msToSeconds = (num) => Math.round(num / 1000);
    // these should work but I found that only isPlaying works
    const STATUSES = ['isBuffering', 'isPlaying', 'didJustFinished', 'isLooping'];
    const getStatus = (playbackEvent) => {
      if (playbackEvent.durationMillis === playbackEvent.positionMillis)
        return 'finished';
      const status = STATUSES.find((status) => playbackEvent[status] === true);
      if (status) return status === 'didJustFinished' ? 'finished' : status;
    };
    
    export default function useAudio({
      uri,
      shouldPlay = false,
      onPlaybackStatusUpdate,
      updateIntervals = 500,
      startPosition = 0,
      shouldLoop = false,
    }) {
      const [sound, setS] = useState(null);
      const [position, setPosition] = useState(0);
      const [status, setStatus] = useState('isLoading');
      const [duration, setDuration] = useState(0);
      
      const handlePlaybackStatusChange = useCallback(
        (playbackEvent) => {
          setStatus(getStatus(playbackEvent));
          setPosition(msToSeconds(playbackEvent.positionMillis||0));
          // because this function is recreated everytime onPlaybuckStatusChange
          // is recreated, onPlaybackStatusUpdate may need to be wrap in an
          // useCallback for better performance
          onPlaybackStatusUpdate?.(playbackEvent);
        },
        [onPlaybackStatusUpdate]
      );
      // cleanup function 
      useEffect(() => {
        return async () => {
          if (sound) {
            // await sound.pauseAsync();
            await sound.unloadAsync();
          }
        };
      }, [sound]);
      // load song on uri changes
      useEffect(() => {
        const loadSound = async () => {
          try {
            const {sound,status} = await Audio.Sound.createAsync(
              uri,
              {
                shouldPlay,
                progressUpdateIntervalMillis: updateIntervals,
                positionMillis: startPosition,
                isLooping: shouldLoop,
                volume:1
              },
              handlePlaybackStatusChange
            );
            setS(sound);
            setDuration(msToSeconds(status.durationMillis));
          } catch (err) {
            console.log(err);
            setStatus('error')
          }
        };
        loadSound();
      }, [
        uri,
        handlePlaybackStatusChange,
        shouldLoop,
        shouldPlay,
        startPosition,
        updateIntervals,
      ]);
      return { position, status, duration,sound };
    }
    

    Using the hook (demo):

    const playOnLoad = true;
    const updateIntervals = 1000;
    const startPosition = 1000 * 60 * 4.4;
    
    export default function App() {
      const { width } = useWindowDimensions();
      const barWidth = width * 0.8;
      const { position, duration, status } = useAudio({
        uri: require('./song.mp3'),
        shouldPlay: playOnLoad,
        updateIntervals,
        startPosition:0,
        shouldLoop: true,
      });
      return (
        <SafeAreaView style={styles.container}>
          <View style={[styles.player, { width: barWidth }]}>
            <Text>Sound status:{status}</Text>
            <Progress.Bar progress={position/duration || 0} width={barWidth} color="green" />
            <View style={styles.timeContainer}>
              <Text style={styles.currentTime}>{formatSeconds(position)}</Text>
              <Text style={styles.duration}>{formatSeconds(duration)}</Text>
            </View>
          </View>
        </SafeAreaView>
      );
    }