skip to Main Content

Problem Description: I recently upgraded my Expo project from SDK 49 to SDK 51, and I’m facing an issue with the onPlaybackStatusUpdate callback in the expo-av library. Prior to the upgrade, everything was working fine on both iOS and Android. However, after upgrading, the onPlaybackStatusUpdate callback only fires when I start or stop playback on Android, but it works correctly on iOS.

Steps to Reproduce:

  1. Create a new Expo project using SDK 51.
  2. Add audio files to your project (e.g., MP3 files).
  3. Implement audio playback using expo-av.
  4. Set up the onPlaybackStatusUpdate callback to monitor playback
    status changes.
  5. Run the app on both Android and iOS devices
  6. Observe that the callback fires as expected on iOS but not
    consistently on Android. Expected Behavior: The
    onPlaybackStatusUpdate callback should fire consistently on both
    Android and iOS, providing updates on playback status (e.g.,
    position, duration, buffering, etc.) every 100ms.

Actual Behavior: On Android, the onPlaybackStatusUpdate callback only fires when playback starts or stops, but not at regular intervals as expected.

Additional Information:

  1. I’ve checked my code thoroughly and ensured that there are no
    logical errors related to the callback registration.

  2. I’ve tested this behavior on multiple Android devices with
    the same result.

  3. The issue seems specific to Android after upgrading to SDK 51.

Environment:

  1. Expo SDK: 51
  2. Platform: Android (iOS works as expected)
    3, Device: Android devices

Code Snippet:

import { Audio } from 'expo-av';

// ... Other setup code ...

const loadAudio = async () => {
  const soundObject = new Audio.Sound();
  try {
    await soundObject.loadAsync(require('./path/to/audio.mp3'));
    soundObject.setOnPlaybackStatusUpdate((status) => {
      console.log('Playback status:', status);
      // Handle playback status updates here
    });
  } catch (error) {
    console.error('Error loading audio:', error);
  }
};

// Call loadAudio() somewhere in your app

I appreciate any insights or solutions to resolve this issue. Thank
you in advance!

2

Answers


  1. I faced the same issue. In a meantime, I added a fix that does that ‘playback update’ logic on Android.
    Here is a code sample:

      // TODO: check whenever expo-av updates. setOnPlaybackStatusUpdate works incorrectly: onPlaybackStatusUpdate is not called during playing audio.
      useEffect(() => {
        if (Platform.OS === 'android' && soundRef.current) {
          const intervalId = setInterval(() => {
            const updatePlaybackStatus = async () => {
              await soundRef.current?.getStatusAsync();
            };
            updatePlaybackStatus();
          }, 500);
          return () => clearInterval(intervalId);
        }
      }, [soundRef.current]);
    
    Login or Signup to reply.
  2. I believe expo-av has undergone some internal changes. While answering this question I do recall encountering audio examples that no longer worked. Also in that answer I created a hook that allows you to fine tune the number of intervals between updates (and other things) Demo:

    import { useState, useEffect, useCallback, useMemo } from 'react';
    import { Audio, usePermissions } from 'expo-av';
    
    // permissions isnt implemented
    
    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 };
    }
    
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search