ryanheise / audio_service

Flutter plugin to play audio in the background while the screen is off.
797 stars 479 forks source link

v2 Android embedder plugins #145

Closed blackraven96 closed 4 years ago

blackraven96 commented 4 years ago

Hi,

Is it possible to update your plugin for being compatible with v2 Android embedder plugins ?

Thx.

ryanheise commented 4 years ago

I believe it is possible, although it would be difficult at this stage.

The migration guide is limited, the Flutter team has so far updated only 2 of its 24 1st class plugins to demonstrate how to support v2, and unfortunately neither of those 2 happen to serve as demonstrative examples for plugins with background tasks.

As such, I would prefer to wait for at least one of their 1st class plugins to be updated that supports background tasks. e.g. android_alarm_manager.

Although, I would welcome a pull request from anyone who is motivated to figure it out from the API documentation.

There also may be a way to support legacy plugins as is (see #138) although again the documentation is a bit lacking at this stage.

shubham-cueclad commented 4 years ago

well than it seems i will have to wait for udates..Thanks a lot you were pretty supportive towards issues thanks again and all the best

xster commented 4 years ago

https://github.com/flutter/flutter/projects/59 is the first party migration state. Most of them are updated but you're right in that android_alarm_manager is the slight exception. We're still currently working on the set of best practices to recommend for users running in headless/background modes (https://github.com/flutter/flutter/issues/32164).

MickaelHrndz commented 4 years ago

Does this mean that I can't use audio_service and android_alarm_manager together for now ? In the same app, they work when used by themselves, but when I use audio_service in a background alarm and it goes off, this exception is thrown :

[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: MissingPluginException(No implementation found for method ready on channel ryanheise.com/audioServiceBackground)
E/flutter ( 7314): #0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:154:7)
E/flutter ( 7314): <asynchronous suspension>
E/flutter ( 7314): #1      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:329:12)
E/flutter ( 7314): #2      AudioServiceBackground.run (package:audio_service/audio_service.dart:813:30)
E/flutter ( 7314): #3      _AsyncAwaitCompleter.start (dart:async-patch/async_patch.dart:47:6)
E/flutter ( 7314): #4      AudioServiceBackground.run (package:audio_service/audio_service.dart:698:26)
...

The android_alarm_manager README says it supports v2 embedding with Flutter >= 1.12. I am on v1.13.6. Is there a way to make it work while waiting for the package updates ? Is this even related or should I open a new issue ?

ryanheise commented 4 years ago

I believe it is related to this issue, yes. Currently, it seems that those who created their projects pre-1.12 are having no issues (like myself), while those who created their projects post-1.12 are facing the problem. So the workaround for now is to use an old project template for your project.

I would ideally like documentation from the Flutter team since time is too short to spend figuring undocumented things out, but if that documentation doesn't come out within the next two weeks (or sooner) I will begin those efforts.

In terms of attracting more attention to this issue, these are the GitHub issues and Medium articles where I have left a comment already:

https://github.com/flutter/flutter/issues/32164 https://github.com/flutter/flutter/issues/47153 https://medium.com/flutter/executing-dart-in-the-background-with-flutter-plugins-and-geofencing-2b3e40a1a124

Simply upvoting/clapping those comments may help increase visibility.

MickaelHrndz commented 4 years ago

Thank you for your quick answer, I will try with an old project. I upvoted and clapped.

MickaelHrndz commented 4 years ago

I tried with a Flutter 1.5.4 project, but unfortunately it didn't work. _PS : I am opening an unrelated issue on justaudio

ryanheise commented 4 years ago

Just to update this issue, I will have a crack at a v2 implementation this weekend.

ryanheise commented 4 years ago

I've just committed a v2 implementation to Git. Hopefully this doesn't break projects still using the old project template, but I would appreciate some feedback if anything is broken for both new and old-style projects.

rohansohonee commented 4 years ago

Hi @ryanheise

To Reproduce Steps to reproduce the behavior:

METADATA

Build.VERSION.SDK_INT: 27 Build.MANUFACTURER: 10or LeakCanary version: 2.1 App process name: com.ryanheise.audioserviceexample Analysis duration: 12492 ms

ryanheise commented 4 years ago

Thanks, @rohansohonee

I'm looking into it.

MickaelHrndz commented 4 years ago

I tried the v2 implementation and the app works like before. However I am still having the issue described in my previous message. I tried in a new empty project and with a different alarm plugin (workmanager). A MissingPluginException is thrown no matter what.

rohansohonee commented 4 years ago

Hi @MickaelHrndz

Visit Upgrading-pre-1.12-Android-projects and follow the Full-Flutter app migration steps. Also use android_alarm_manager plugin as it has v2 support.

MissingPluginException might be fixed if you add this to your MainActivity.java:

public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
   GeneratedPluginRegistrant.registerWith(flutterEngine);
}

Hope this is helpful.

ryanheise commented 4 years ago

Also I believe this can be fixed by using a sufficiently recent version of Flutter where plugin registration will be done automatically through reflection. If it's not in stable now, it should be soon (or you can try beta).

MickaelHrndz commented 4 years ago

Hi @rohansohonee, thank you for the answer. I followed the guide but it didn't solve the issue. @ryanheise thx for the answer as well. I tried on the latest stable & beta versions but still no luck. To be clear, this is the line raising the error in my code (only when used in the alarm callback) :

AudioServiceBackground.run(() => PlayerBackgroundTask());

And this is the code I use in the new testing project :

main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await AndroidAlarmManager.initialize();
  runApp(MyApp());
  await AndroidAlarmManager.periodic(const Duration(seconds: 10), 0, backgroundTaskEntrypoint);
}

void backgroundTaskEntrypoint() => AudioServiceBackground.run(() => MyBackgroundTask());

class MyBackgroundTask extends BackgroundAudioTask {
  @override
  Future<void> onStart() async {}
  @override
  void onStop() {}
  @override
  void onPlay() {}
  @override
  void onPause() {}
  @override
  void onClick(MediaButton button) {}
}

The exception thrown is identical in both projects :

MissingPluginException(No implementation found for method ready on channel ryanheise.com/audioServiceBackground)

dkobia commented 4 years ago

@MickaelHrndz confirmed on both dev and beta channels.

ryanheise commented 4 years ago

@dkobia @MickaelHrndz can you reproduce this on the audio_service example? I was not able to.

I added workmanager: ^0.2.0 to pubspec.yaml, ran the example and pressed play, and it did not complain about a missing plugin. As mentioned, I am using the latest update on the beta channel. Please try the same modification to the audio_service example on your own end and let me know if you run into the problem.

If it works when modifying the example in this way but does not work in your own project, then you can compare the two projects to spot the difference and maybe re-check everything in the v2 migration guide:

https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects

I am not sure what specifically is the cause, but the most important thing is to add this to your manifest:

<meta-data
    android:name="flutterEmbedding"
    android:value="2" />

But there are other more subtle things such as to remove this:

<meta-data
                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                android:value="true" />

The problem is that this references the old (pre-v2) class name and may drag in classes that can cause problems.

MickaelHrndz commented 4 years ago

Using the audio_service example project, with the code :

main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await AndroidAlarmManager.initialize();
  runApp(MyApp());
  await AndroidAlarmManager.periodic(const Duration(seconds: 1), 0, backgroundTaskEntrypoint);
}

void backgroundTaskEntrypoint() {
  print("###### 1 ######");
  AudioServiceBackground.run(() => MyBackgroundTask());
} 

class MyBackgroundTask extends BackgroundAudioTask {
  @override
  Future<void> onStart() async {
    print("######  2  ######");
  }
  @override
  void onStop() {}
  @override
  void onPlay() {}
  @override
  void onPause() {}
  @override
  void onClick(MediaButton button) {}
}

If I don't add the android_alarm_manager service and receivers in the manifest, # 1 # and # 2 # are never printed and no error is thrown. "AlarmService started!" is still printed though.

If I do, # 1 # is printed but the same error is thrown by the same line, and therefore # 2 # is not printed.

So I guess the issue really stems from android_alarm_manager ?

ryanheise commented 4 years ago

@MickaelHrndz I didn't realise you were using audio_service in that way, but this is definitely not supported. i.e. Your backgroundTaskEntrypoint looks like the entrypoint for AudioService.start but you are using it as the entrypoint for android_alarm_manager. Different plugins do not understand each other's entrypoints, so you must create a dedicated entrypoint for android_alarm_manager and a dedicated entrypoint for audio_service.

Stepping back and looking at what you're trying to do, it seems you want to periodically play a sound in the background every second. There are a few problems with using audio_service to do this.

  1. audio_service wasn't designed to be started and stopped with such frequency because it's a foreground service. It will show a media-style notification that itself takes some time to appear and disappear. If your audio is so short that it can be played once per second, then there is no need for audio_service anyway, you should be able to get away with just playing the audio without wrapping that audio in audio_service.
  2. If you tried to rectify the problem where you never actually called AudioService.start, the problem is that this plugin is designed such that AudioService.start must be started from the UI. Looking at your use case, I guess you'd probably want to start it within the periodic callback for the alarm manager, but since that is not the UI isolate, I doubt it would work. If it doesn't work, it's also not a priority for me to address that use case (although I might consider a pull request if there is a clean way to support it).
ryanheise commented 4 years ago

@rohansohonee regarding the memory leak, this appears to be a bug in the Flutter engine (see https://github.com/flutter/engine/pull/16204) but if there is a workaround before that pull request lands, I'll implement it.

ryanheise commented 4 years ago

@rohansohonee Here is a workaround for the leak:

public class MainActivity extends FlutterActivity {
  @Override
  public FlutterEngine provideFlutterEngine(Context context) {
    // Instantiate a FlutterEngine.
    FlutterEngine flutterEngine = new FlutterEngine(context.getApplicationContext());

    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    );

    return flutterEngine;
  }
}

What this does is override the instantiation of the main activity's FlutterEngine because the one that the framework instantiates for us retains a reference to your main activity. The code above works around this by passing in the application context rather than the activity instance to the constructor parameter of FlutterEngine. You can also find another way to do this here (but make sure you change the parameter to FlutterEngine in the same way as above):

https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#step-3-optional-use-a-cached-flutterengine

This workaround should not be needed once they fix it upstream.

If that works for you, I'll likely publish a new release with the v2 implementation, and can sort out any remaining issues after that release.

MickaelHrndz commented 4 years ago

@ryanheise The 1s delay was to avoid waiting while testing. I am building a radio app that lets you optionally set an alarm clock to wake up to it. That's why I need to use audio_service in android_alarm_manager. If I try to use AudioService inside the alarm callback, the error thrown is the same, except for the method :

No implementation found for method isRunning on channel ryanheise.com/audioService

What would I use if not AudioServiceBackground.run(() => PlayerBackgroundTask(play)); ?

rohansohonee commented 4 years ago

Hi @MickaelHrndz

You might have to implement your own media notification. For audio playback you can use just_audio plugin. This solution may not be the best but can work for now in my opinion.

MickaelHrndz commented 4 years ago

@rohansohonee I guess but I'm already using just_audio wrapped with audio_service and it is working just fine. I don't want to rewrite my code and a whole plugin, just to run into the same issue I have here. But yeah, just_audio is working in the alarm callback.

If you meant a different notification specifically for the alarm, it might actually work and be good enough. I will give it a try, thanks for the idea.

rohansohonee commented 4 years ago

Hey guys just letting you all know a new version of Flutter has been pushed on stable channel. Please do upgrade flutter and post your report.

rohansohonee commented 4 years ago

@ryanheise I will wait until they fix it in upstream.

ryanheise commented 4 years ago

@MickaelHrndz OK, I have a clearer picture of your use case now. That is not actually allowed on Android due to background service limitations:

https://developer.android.com/about/versions/oreo/background.html#services

So the only way you can really achieve what you're trying to achieve is to keep the audio service running the whole time in the background, effectively on "pause", and start playing at the scheduled time, and then you would essentially do away with alarm manager. I know this is not an ideal solution since it will unnecessarily use battery when it could be sleeping. To implement this properly, you may need to get your hands dirty with some Android programming and research whether there is a certain way to build this use case on Android.

MickaelHrndz commented 4 years ago

@ryanheise Interesting. Is there a way to hide the audio_service notification ?

I can make it work with another notification from flutter_local_notifications, but the player doesn't stop when the alarm in cancelled. I need to either detect from the callback when the application is started to stop its player, or being able to call something like AudioPlayer.disposeAll(). This might be related to the fact that a player isn't stopped / disposed when you restart the app (with shift +r), resulting in multiple audios being played. Should I open a new issue on the just_audio repo ?

ryanheise commented 4 years ago

@MickaelHrndz You can't hide the notification. This is another requirement imposed by Android when you start a foreground service. You have to let the user know that you're consuming resources in the background, and this notification is the way this is done.

I don't know a way to hook into hot restart and dispose resources.

rohansohonee commented 4 years ago

@MickaelHrndz

I think i might have a solution for this use case. Follow these steps:

  1. When your alarm fires you will enter backgroundTaskEntryPoint().
  2. Inside backgroundTaskEntryPoint() you will need to launch your FlutterActivity and pass some parameter which tell's that the alarm was fired (this will allow you to check in dart code that alarm has fired and you need to play audio).
  3. Once the FlutterActivity starts and app get's launched you can start the audio_service.

You may need to do some android coding to launch an Activity from the background isolate. But once you manage to launch the activity along with some parameter in the activity's intent you should be easily able to start the audio_service without any issues. Hope this is helpful.

ryanheise commented 4 years ago

@MickaelHrndz @dkobia Are either of you still getting the MissingPluginException? If not, I will close this issue.

MickaelHrndz commented 4 years ago

@rohansohonee That's an even better idea, thank you. I tried with both the flutter_appavailability and a custom method channel, but I still get this dreaded MissingPluginException. I would think I can't interact with the platform code at all inside the alarm callback, if it weren't for just_audio actually working. I'm pretty clueless.

@ryanheise since the ones I get are not really related to audio_service itself, it's fine by me. Thank you again for your help.

rohansohonee commented 4 years ago

@MickaelHrndz The reason flutter_appavailability may not be working is:

  1. It uses registrar.activity during registration. It should instead use registrar and then get the applicationContext from registrar i.e registrar.getApplicationContext. (Please inform the author of the plugin to update)
  2. The v2 embedding might also be a reason. Please ensure you have performed flutter clean, you are on latest flutter stable channel and you have performed plugin registration.

IMPORTANT

<meta-data
    android:name="flutterEmbedding"
    android:value="2" />

and your MainActivity.java

public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
   GeneratedPluginRegistrant.registerWith(flutterEngine);
}

If you still keep getting MissingPluginException then take this issue up with the flutter team.

NOTE: doing a flutter clean, running pub get packages or upgrading flutter can sometimes fix these kind of issues.

rohansohonee commented 4 years ago

@ryanheise I believe this issue can be closed now.

ryanheise commented 4 years ago

Closing.

Note: this is feature has now been published as release 0.6.0.

dkobia commented 4 years ago

@ryanheise yes thanks the issue was resolved. Interestingly the MissingPluginException was caused by another audio plugin that seems to break v2 reflection altogether -- https://pub.dev/packages/audiofileplayer -- not sure how or why.

github-actions[bot] commented 3 years ago

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.