React-native-track-player: [Android] Player doesn't react as well as it could to other audio apps.

Created on 21 Feb 2019  路  10Comments  路  Source: react-native-kit/react-native-track-player

Even when implementing the remote-duck event logic suggested in the docs, the player doesn't react as well it seems it could to other audio apps.

My app is a podcast style app, so I have opted to always pause/resume rather than adjusting volume.

Here is my remote-duck handler:

TrackPlayer.addEventListener('remote-duck', data => {
  console.log('remote-duck', data);
  let { paused: shouldPause, ducking: shouldDuck } = data;
  if (shouldPause || shouldDuck) {
    await TrackPlayer.pause();
    didPauseForDucking = true;
  } else {
    if (didPauseForDucking) {
      await TrackPlayer.play();
    }
  }
});

//Note: didPauseForDucking is reset to false whenever the player state changes to playing

Here are some symptoms I have observed:

(1) remote-duck event not always fired the second time another audio app interrupts

  1. Launch my TrackPlayer based app
  2. Start playing a track
  3. Background my app, track continues to play as expected
  4. Launch Spotify
  5. Play Spotify track
  6. Remote-duck event is received, TrackPlayer track stops, Spotify track begins
  7. Pause Spotify
  8. Switch back to TrackPlayer app
  9. Play TrackPlayer track
  10. Switch back to Spotify
  11. Play Spotify track
  12. Remote-duck event is NOT received, Spotify track plays over TrackPlayer track

For some reason Spotify misbehaves in this way, but Pandora does not. However when trying the same thing switching between Pandora and Spotify, it DOES work properly.

(2) TrackPlayer doesn't alert other apps to duck

  1. Launch my TrackPlayer based app
  2. Start playing a track
  3. Background my app, track continues to play as expected
  4. Launch Spotify or Pandora app
  5. Play a Spotify or Pandora track
  6. Remote-duck event is received, TrackPlayer track stops, Spotify or Pandora track begins
  7. Multitask back to TrackPlayer app
  8. Press play to resume TrackPlayer track
  9. TrackPlayer track plays over top of the Spotify or Pandora track

When trying this same scenario using Spotify and Pandora instead, it behaves as expected. Playing Spotify stops Pandora, and playing Pandora stops Spotify. So, these apps are definitely both doing whatever necessary to let other apps know they are taking over, and also listening for the appropriate times to stop playing.

Edit: I numbered the cases above

Android Bug

Most helpful comment

This is what I am using:
```
TrackPlayer.addEventListener('remote-duck', async data => {
// debug.log('remote-duck', data);

let { paused: shouldPause, permanent: permanentLoss = false } = data;

// iOS:
// When using iosCategoryMode: 'spokenAudio',
// audio is automatically paused for an interruption like GPS directions.
// The pause comes BEFORE this event.
// However, audio is not automatically resumed.
// The permanent parameter is not used on iOS, only the paused parameter.
// We will assume that if paused is false, that we should resume,
// and that iOS is smart enough to only send this event if the
// interruption is what caused the pause in the first place.
// So, the only thing we need to do is resume if needed.
if (Device.isIos) {
  if (!permanentLoss && !shouldPause) {
    store.dispatch(audioPlayerPlay());
  }
}

// Android:
// When using alwaysPauseOnInterruption: true,
// audio does not automatically duck or pause.
// Instead, it forces remote-duck to happen.

if (Device.isAndroid) {
  let playerState = await AudioPlayer.getPlayerState();

  if (shouldPause) {
    store.dispatch(audioPlayerPause());
    if (playerState === AudioPlayer.STATE_PLAYING) {
      didPauseTemporarily = !permanentLoss;
      if (didPauseTemporarily) {
        didPauseTemporarilyTime = Date.now();
      }
    } else {
      didPauseTemporarily = false;
    }
  } else if (didPauseTemporarily) {
    //only resume playback if
    //this was a temporary interruption AND
    //the state is paused AND
    //less than 30 seconds have elapsed since pausing
    if (playerState === AudioPlayer.STATE_PAUSED) {
      let secondsSincePause = (Date.now() - didPauseTemporarilyTime) / 1000;
      if (secondsSincePause < 30) {
        store.dispatch(audioPlayerPlay());
      }
    }
    didPauseTemporarily = false;
  }
}

});```

All 10 comments

After looking at the native code a bit, I think I have determined that both of these cases are related to TrackPlayer not tracking the loss of audio focus when it is paused.

Things start behaving much better if I stop() when receiving a remote-duck event instead of pause().

Trouble is, I really would like to use pause() so that my users can pickup right where they left off.

Adding a couple lines to set hasAudioFocus in the onAudioFocusChange method seems to result in the behavior I need for my app, but I'm not sure if it is the best fix. This feels like it could end up with unexpected behavior if you just wanted to change audio volume or some other reaction to these events.

@Override
public void onAudioFocusChange(int focus) {
    Log.d(Utils.LOG, "onAudioFocusChange, focus: " + focus);
    Log.d(Utils.LOG, "onDuck");

    boolean paused = false;
    boolean ducking = false;

    switch(focus) {
        case AudioManager.AUDIOFOCUS_LOSS:
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            paused = true;
            //added
            hasAudioFocus = false;
            //-----
            break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            ducking = true;
            //added
            hasAudioFocus = false;
            //-----
            break;
        case AudioManager.AUDIOFOCUS_GAIN:
            break;
    }

    Bundle bundle = new Bundle();
    bundle.putBoolean("paused", paused);
    bundle.putBoolean("ducking", ducking);
    service.emit(MusicEvents.BUTTON_DUCK, bundle);
}

Perhaps it would be more appropriate to add some way to abandonFocus() intentionally? Or maybe pausing should abandonFocus()??? Sorry, I wish I understood enough to provide a more confident universal solution.

One more thought. If the above DOES happen to be a decent solution. It is unclear to me if hasAudioFocus should be set to true in the AudioManager.AUDIOFOCUS_GAIN case.

Basically, there are three types of audio focus loss:

  • Transient Loss: The app needs to pause the track for a moment, but it should be prepared to resume it shortly.

    • Commonly seen with small audio clips from messaging apps.

    • Recommended action: pause()

  • Transient Loss "can duck": The app should pause or just lower the volume, but it should be prepared to resume it shortly.

    • Commonly seen with notification sound effects.

    • Recommended action: setVolume(...)

  • Loss: The app should stop and release its resources, it won't be coming back automatically. The user may manually open the app and start again, but there are no guarantees.

    • Commonly seen when another music app starts playing.

    • Recommended action: stop()

There were the paused and ducking properties which allowed the app to distinguish between the transient losses, but there weren't any way to detect a permanent loss. Because of that, I've added the permanent property.

On top of that, I abandon the audio focus when a permanent loss happens, so it will work even if you pause the track instead of stopping it.

Let me know if 237f5329c8761f373e149ad2a3e0f8b9c1ef4920 fixes it for you.

Thanks for the detailed info and the fix.

I won't be able to test this right away. We are in the middle of a release cycle, and our app already went through QA with my temporary solution.

I'll report back when I get a chance to test this fix.

After some light testing this appears to work great. :)

For my app, I ended up pausing the player when either paused or ducking is true. And also setting a boolean flag when permanent is false, then resuming playback when all 3 values are false, and that flag is set.

@curiousdustin - Please could you provide me the ducking eventlistener code that solved the issue? Thank you!

This is what I am using:
```
TrackPlayer.addEventListener('remote-duck', async data => {
// debug.log('remote-duck', data);

let { paused: shouldPause, permanent: permanentLoss = false } = data;

// iOS:
// When using iosCategoryMode: 'spokenAudio',
// audio is automatically paused for an interruption like GPS directions.
// The pause comes BEFORE this event.
// However, audio is not automatically resumed.
// The permanent parameter is not used on iOS, only the paused parameter.
// We will assume that if paused is false, that we should resume,
// and that iOS is smart enough to only send this event if the
// interruption is what caused the pause in the first place.
// So, the only thing we need to do is resume if needed.
if (Device.isIos) {
  if (!permanentLoss && !shouldPause) {
    store.dispatch(audioPlayerPlay());
  }
}

// Android:
// When using alwaysPauseOnInterruption: true,
// audio does not automatically duck or pause.
// Instead, it forces remote-duck to happen.

if (Device.isAndroid) {
  let playerState = await AudioPlayer.getPlayerState();

  if (shouldPause) {
    store.dispatch(audioPlayerPause());
    if (playerState === AudioPlayer.STATE_PLAYING) {
      didPauseTemporarily = !permanentLoss;
      if (didPauseTemporarily) {
        didPauseTemporarilyTime = Date.now();
      }
    } else {
      didPauseTemporarily = false;
    }
  } else if (didPauseTemporarily) {
    //only resume playback if
    //this was a temporary interruption AND
    //the state is paused AND
    //less than 30 seconds have elapsed since pausing
    if (playerState === AudioPlayer.STATE_PAUSED) {
      let secondsSincePause = (Date.now() - didPauseTemporarilyTime) / 1000;
      if (secondsSincePause < 30) {
        store.dispatch(audioPlayerPlay());
      }
    }
    didPauseTemporarily = false;
  }
}

});```

@curiousdustin - thanks for the sample code!

@curiousdustin just to reach out and say thanks for your input on this, and for your sample code. This is by no means an easy topic, with all the native handlers, various interruption mechanisms, and edge-cases (like you describe in your 30 second example). Your code is super helpful! So thanks!

If you have the time, maybe it would be great to add a separate section in the documentation about your way of dealing with it (include the flow diagram found here:
-https://github.com/react-native-kit/react-native-track-player/issues/611#issuecomment-519567397

  • and your code examples

which would likely help other people who have been very confused by the audio-ducking flow - I only managed to get it working by trawling through the issues on the library.

Either way, thanks a bunch! 馃殌

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EhteshamAnwar picture EhteshamAnwar  路  3Comments

elioscordo picture elioscordo  路  3Comments

sagargheewala picture sagargheewala  路  3Comments

JakeMotta picture JakeMotta  路  3Comments

RiccardoNL picture RiccardoNL  路  3Comments