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:
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.
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
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:
pause()setVolume(...)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
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! 馃殌
Most helpful comment
This is what I am using:
```
TrackPlayer.addEventListener('remote-duck', async data => {
// debug.log('remote-duck', data);
});```