Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] Kill the AudioServiceBackground Isolate when the app is destroyed #159

Closed
vinicius-animo opened this issue Jan 23, 2020 · 29 comments
Assignees
Labels

Comments

@vinicius-animo
Copy link

Is your feature request related to a problem? Please describe.
On Android, when the app is closed (or crashes) while the audio is playing, the user may expect that the audio stops immediately.
In the latest version of the plugin, the AudioServiceBackground keeps running and the user needs to tap the stop button in the notification bar in order to make it happen.

Describe the solution you'd like
It would be nice if the start method had a flag such as persist: false to control the Isolate behavior in those conditions (app killed, closed or crashed).

Does Android offer this kind of control over the app states?

@ryanheise ryanheise self-assigned this Jan 24, 2020
@ryanheise
Copy link
Owner

There are various ways to detect when an activity is destroyed (e.g. pressing the back button from the root route) and when a task is swiped up from the task manager using platform-side APIs. I'm assuming this is what you mean by closing the app?

Since the plugin was designed to continue running in the background, the current behaviour is as intended, as backing out of the activity is a common action when the user wants to multitask while continuing to listen to the audio in your app, but it has the side effect of destroying the activity. Here, even though the activity is destroyed, we want the audio to continue, and this is how mp3 players tend to work. When swiping up a task in the task manager, it is also the usual behaviour in mp3 players to just destroy the activity but still have the music playing in the background. However, in this case I can conceive of it being useful to have an option for this behaviour. So these two different ways of "closing the app" might need to have different options.

For crashing, I don't know if there is a reliable way to detect that, and you might want to address it by putting more try/catch blocks in your app.

But otherwise, this is a feature I could consider adding.

@vinicius-animo
Copy link
Author

Yes, you're correct: crashing should not be listed here in the requirements and should be handled properly in the app logic.

Back to the feature request, a few comments:

I would add as another argument for the persistent: true|false flag: the UX consistency.
Android and iOS differ on how the audio is handled when the app is killed. Android keeps the background task running, while iOS kills it instantly.

Another point is that some (if not most) of the users are currently familiar with non-persistent audio players when killing the app using the Task Manager. Think about how Spotify and YouTube (premium) handle this situation. I'd agree that it is not necessarily a requirement for every project, but it is a pretty common scenario.

@volgin
Copy link

volgin commented Jan 24, 2020

As a user, I would uninstall and downvote any app that keeps playing after I closed it.

@ryanheise
Copy link
Owner

@vinicius-animo sounds reasonable.

@volgin To my knowledge, audio_service implements the most common, expected behaviour for music players on iOS and Android respectively, so so my primary consideration would be to ensure that those standard behaviours continue to be possible (currently they're the only supported way), and then consider everything else as options.

@volgin
Copy link

volgin commented Jan 25, 2020

@ryanheise Apple Music, Spotify and Amazon Prime Music stop playing immediately after an app is closed. I am not familiar with any music player that continues to play after it was closed. I am not sure if I understood you correctly, but this is the most common, expected behavior.

@ryanheise
Copy link
Owner

@volgin iOS and Android do have different historical behaviours here, so what you describe as the most common, expected behaviour might be true on iOS, but historically it has not been true on Android where even today, Google's official Android music player "Google Play Music" continues to play music in the background after you destroy the activity or swiping away the task. So swiping away the task is really just a way to free up memory taken up by the activity. If you wanted to actually stop the music, you should instead press the "stop" button or the x button in the notification. This is common behaviour and dates back to the oldest versions of Android where the mechanism Android provided to allow apps to play music in the background was to create a separate background component called a "service" which was designed to continue running even after the UI component (the "activity") was destroyed. It was even advised in the developer documentation that this is how you should build a music player app, and the task APIs that would make it possible to emulate iOS behaviour weren't available until much later.

I would say this behaviour is still prevalent on Android today, particularly in music and podcast players where the background service just plays audio and can be fully detached from the user interface. But Android is rather lax on standards and doesn't really enforce U/X design guidelines in the same way Apple does, so there is also a bit of variety on Android, and that is why an option was being considered above. Such an option would give developers the freedom to make their own U/X choices, and even to be able to emulate the iOS behaviour on Android if they wanted to, or to go with the historical behaviours for the respective platforms. I believe the plugin's current behaviour does follow the expected behaviour on iOS, although please let me know if that is not the case. For the Android side, the option to emulate the iOS behaviour is actively being considered in this issue.

@vinicius-animo Regarding the proposed option(s) for Android, just to clarify what you mean by closing the app, this could mean two things:

  1. When you press the back button on the main activity on Android, it destroys the activity and the service continues to run independently.
  2. When you swipe away the task in the task manager on Android, it destroys the activity and the service continues to run independently.

I am inclined to only add an option for (2) for now because I don't see a compelling use case for an option for (1). On Android, if you back out of the main activity and the activity is destroyed, but then the service is also destroyed, I think the user would be quite annoyed if it were a music player. For (2), I can see both ways. If the user swipes away Google Play Music in the task manager, it frees up memory taken by the Google Play Music app user interface, but to stop the music you would just press the stop button. If you swipe away Spotify in the task manager, it frees up the UI memory but also stops the music. At least this should be provided as an option to the developer. But do you have a compelling use case for making an option for (1) or would you be satisfied to just have an option for (2)?

@volgin
Copy link

volgin commented Jan 25, 2020

@ryanheise Thank you for pointing out Google Play Music - I never tried it before.

I think that app developers already have control over the back button behavior, and can close/stop the background service in their app logic, if they choose so.

@Camerash
Copy link
Contributor

Camerash commented Feb 2, 2020

Any updates on this issue?

To me, I think what @ryanheise mentioned as the second option would satisfy my needs as a developer.

I also tried to implement it with the existing API:
I have tried to add android:stopWithtask="true" inside the audio service xml tag in AndroidManifest.
The behaviour this produces is: After swiping the app from the task manager away, the service notification dismissed, however the audio is still playing because no information is given to flutter when the service is destroyed.

I think we can add the persist flag, and send stop action to flutter when persist == true in onTaskRemoved?

@Camerash
Copy link
Contributor

Camerash commented Feb 3, 2020

I might be able to do a PR to implement the feature. Is this still a wanted feature?

@ryanheise
Copy link
Owner

I also tried to implement it with the existing API:
I have tried to add android:stopWithtask="true" inside the audio service xml tag in AndroidManifest.
The behaviour this produces is: After swiping the app from the task manager away, the service notification dismissed, however the audio is still playing because no information is given to flutter when the service is destroyed.
I think we can add the persist flag, and send stop action to flutter when persist == true in onTaskRemoved?

That's correct, onTaskRemoved is the callback to handle swiping away the task.

I might be able to do a PR to implement the feature. Is this still a wanted feature?

Definitely, I think it makes sense to give users a choice for the Android behaviour. Pull requests are certainly welcome since I have quite a few high priority issues to work on first before I would get around to this one.

Since this is an Android-specific option, I would name the parameter something like androidStopOnRemoveTask with a default value of false (which is the default behaviour on Android). Looking at the current implementation of onTaskRemoved I am a bit suspicious about whether I handled the existing case correctly:

  @Override
  public void onTaskRemoved(Intent rootIntent) {
    MediaControllerCompat controller = mediaSession.getController();
    if (androidStopForegroundOnPause && controller.getPlaybackState().getState() == PlaybackStateCompat.STATE_PAUSED) {
      stopSelf();
    }
    super.onTaskRemoved(rootIntent);
  }

Basically that existing code is so that if the user wants it, the service will be shut down when swiping it away "while" in pause mode, which is another behaviour that some users want. However, simply calling stopSelf doesn't actually shut down the isolate and I don't think people noticed it simply because the audio was already paused. What really needs to happen to cause the isolate to shut down is this:

listener.onStop();

This will send a message to the isolate to gracefully stop.

@Camerash
Copy link
Contributor

Camerash commented Feb 3, 2020

@ryanheise In that case I will fix the old case as well. Does the following code make sense to you?

@Override
public void onTaskRemoved(Intent rootIntent) {
  MediaControllerCompat controller = mediaSession.getController();
  if (androidStopOnRemoveTask || (androidStopForegroundOnPause && controller.getPlaybackState().getState() == PlaybackStateCompat.STATE_PAUSED)) {
    listener.onStop();
  }
  super.onTaskRemoved(rootIntent);
}

EDIT: stopSelf() turns out to not be required, since after notifying client with onStop(), the onStart method should complete and the rest will be handled by the stopped callback

@ryanheise
Copy link
Owner

Looks good to me. Yes, sorry I should have clarified in my previous comment that after listener.onStop(); triggers a graceful shutdown, that should eventually result in the service being stopped. But the true test is after trying out. If it works for you, I'd be happy to integrate the PR.

@Camerash
Copy link
Contributor

Camerash commented Feb 3, 2020

PR is ready to merge: #165

@ryanheise
Copy link
Owner

Thanks, @Camerash ! I've merged your PR. @vinicius-animo and @volgin you can test this on the master branch, and turn on the feature by passing in androidStopOnRemoveTask: true into AudioService.start().

@vinicius-animo
Copy link
Author

Works perfectly! Thanks @Camerash and @ryanheise.
Tomorrow I'll test the stability on iOS and post an update here.

Btw, I had to replace audioplayers with just_audio after doing some research and bumping into this issue.
Also to be tested on iOS. But so far, great plugin, @ryanheise :)

@vinicius-animo
Copy link
Author

Update on iOS: works perfectly.

@ryanheise
Copy link
Owner

Thanks @vinicius-animo , glad to hear it!

@ElaineSchwner
Copy link

Good life, fellows!!!
Thanks!!
How do I add this to my code?
I am using audio_service: ^0.7.1 and just_audio: ^0.1.4.
Great work!
But it doesn't stop playing after the user swipe up and kill the app.
How do I make that?
Thank you!

@Camerash
Copy link
Contributor

Just turn on the feature by passing in androidStopOnRemoveTask: true into AudioService.start()
If it is not working, try providing some of your code snippets?

@renanmgs
Copy link

This was removed? I'm using just_audio: ^0.2.2 and audio_service: ^0.11.0 and the only options i have in AudioService.start() with the android prefix are:

androidEnableQueue: ,
androidNotificationChannelName: ',
androidNotificationChannelDescription: ,
androidNotificationColor: ,
androidNotificationIcon: '',
androidStopForegroundOnPause: , (tried this but with no succes)
androidArtDownscaleSize: ,
androidNotificationClickStartsActivity: ,
androidNotificationOngoing: ,
androidResumeOnClick: ,

I tried to use the WidgetsBindingObserver and on the AppLifecycleState.detached: i called AudioService.stop(); but it dosent work too, it just leak.

@renanmgs
Copy link

renanmgs commented Jun 24, 2020

Well, to anyone wondering, i just got it working:
On your BackgroundAudioTask simply put:

@override
  void onTaskRemoved() {
    // TODO: implement onTaskRemoved
    onStop();
    super.onTaskRemoved();
  }

Its not needed anymore to use androidStopOnRemoveTask: true.

@dtdevpro
Copy link

dtdevpro commented Jul 7, 2020

When I swipe away the task in the task manager on Android, it destroys the activity and the service continues to run independently:
I code:
AudioService.customEventStream.listen((event) async {
switch (event){
case "stop":
await audioPlayer.stop(); // not working
_playing = false;
break;
}
});

@OverRide
Future onStop() async {
AudioServiceBackground.sendCustomEvent("stop");
await super.onStop();
}
void onTaskRemoved() {
onStop();
super.onTaskRemoved();
}

But onstop() only stops the service, not the audio player. Help me fix!!!

@ryanheise
Copy link
Owner

From the documentation for onStop:

You should implement this method to stop playing audio and dispose of any resources used.

@dtdevpro
Copy link

dtdevpro commented Jul 7, 2020

But Audio Player is in Flutter UI, I stop audio:
AudioService.customEventStream.listen((event) async {
switch (event){
case "stop":
await audioPlayer.stop(); // not working
_playing = false;
break;
}
});

audioPlayer.stop() not working: because flutter UI is destroys?
Please show me how to stop audio player!

@ryanheise
Copy link
Owner

So you're not using this plugin in the way it was intended? From the README:

How does this plugin work?

You encapsulate your audio code in a background task which runs in a special isolate that continues to run when your UI is absent.

Please read the documentation in both instances to make sure you are using the plugin correctly.

@qianyukun
Copy link

Well, to anyone wondering, i just got it working:
On your BackgroundAudioTask simply put:

@override
  void onTaskRemoved() {
    // TODO: implement onTaskRemoved
    onStop();
    super.onTaskRemoved();
  }

Its not needed anymore to use androidStopOnRemoveTask: true.

it worked

@EArminjon
Copy link

EArminjon commented Sep 20, 2021

Seems not working as espected since 0.18 and Android 11.

 audioHandler = await AudioService.init(
        config: const AudioServiceConfig(
          androidNotificationOngoing: true,
          androidStopForegroundOnPause: true,
          androidNotificationChannelName: "Lecteur de musique",
          androidNotificationChannelDescription: "Cette notification doit être activée pour afficher le player dans la zone de notification",
        ),
        builder: () => AudioServiceTask(),
      );
class AudioServiceTask extends BaseAudioHandler {
  AudioPlayer _player = AudioPlayer();

  AudioServiceTask() {
    _player.playbackEventStream.map(_transformEvent).pipe(playbackState);
  }

  @override
  Future<void> onTaskRemoved() async {
    await stop();
    super.onTaskRemoved();
  }

  PlaybackState _transformEvent(PlaybackEvent event) {
    return PlaybackState(
      controls: [
        if (_player.playing) MediaControl.pause else MediaControl.play,
        MediaControl.stop,
      ],
      systemActions: const {},
      androidCompactActionIndices: const [0, 1],
      processingState: const {
        ProcessingState.idle: AudioProcessingState.idle,
        ProcessingState.loading: AudioProcessingState.loading,
        ProcessingState.buffering: AudioProcessingState.buffering,
        ProcessingState.ready: AudioProcessingState.ready,
        ProcessingState.completed: AudioProcessingState.completed,
      }[_player.processingState]!,
      playing: _player.playing,
      updatePosition: _player.position,
      bufferedPosition: _player.bufferedPosition,
      speed: _player.speed,
      queueIndex: event.currentIndex,
    );
  }

  @override
  Future<void> playMediaItem(MediaItem mediaItem) async {
    await _player.pause();
    await _player.setUrl(mediaItem.id);
    await _player.play();
    this.mediaItem.add(mediaItem);
  }

  @override
  Future<void> updateMediaItem(MediaItem mediaItem) async {
    this.mediaItem.add(mediaItem);
  }

  @override
  Future<void> pause() => _player.pause();

  @override
  Future<void> play() async {
    await _player.seek(null);
    await _player.play();
  }

  @override
  Future<void> stop() async {
    await _player.stop();
  }

  @override
  Future<void> click([MediaButton button = MediaButton.media]) async {
    switch (button) {
      case MediaButton.media:
        if (playbackState.value.playing == true) {
          await pause();
        } else {
          await play();
        }
        break;
      case MediaButton.next:
        await skipToNext();
        break;
      case MediaButton.previous:
        await skipToPrevious();
        break;
    }
  }
}

If the music is playing and if we stop it throug the notification button, the notification didn't disappear but go in lower priority mode and stay visibile.
If the music is not playing (pause) and if we stop it through the notification button, the notification well disappear.

Please, try many time (3/4) it seems to be a little random.

@ryanheise
Copy link
Owner

@EArminjon

  1. This issue is closed.
  2. Your issue is not the same issue as this one.

I'm not even sure from what you're saying whether this is really a bug, but if it is a bug, please don't place a comment somewhere in the backlog of closed issues, please open a new issue and make sure you provide all of the information requested in the template so that I can investigate it.

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs, or use StackOverflow if you need help with audio_service.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

9 participants