ryanheise / audio_service

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

[Example] Pass data to backgroundAudioPlayerTask #8

Closed akdasa closed 5 years ago

akdasa commented 5 years ago

Good getlocaltime()!

I'm struggling to pass any data to _backgroundAudioPlayerTask function (according to this example https://github.com/ryanheise/audio_service/blob/master/example/lib/main.dart). I want to pass URL to play.

var res = AudioService.start(
  backgroundTask: () async {
    // I modified CustomAudioPlayer also
    CustomAudioPlayer player = CustomAudioPlayer("HERE IS MY URL");
    AudioServiceBackground.run(
      onStart: player.run,
      onPlay: player.play,
      onPause: player.pause,
      onStop: player.stop,
      onClick: (MediaButton button) => player.playPause(),
    );
  },
  // omited
);

But it doesn't work. It returns instance of Future<bool> which resolves to false:

I/flutter (19613): Instance of 'Future<bool>'
I/flutter (19613): false

I also was trying to do something like this:

Function play(String url) {
  void handler() async {
    CustomAudioPlayer player = CustomAudioPlayer(url);
    AudioServiceBackground.run(
      onStart: player.run,
      onPlay: player.play,
      onPause: player.pause,
      onStop: player.stop,
      onClick: (MediaButton button) => player.playPause(),
    );
  }
  return handler;
}

Error:

E/flutter (20407): [ERROR:flutter/shell/common/shell.cc(184)] Dart Error: Unhandled exception:
E/flutter (20407): NoSuchMethodError: No top-level getter 'handler' declared.
E/flutter (20407): Receiver: top-level
E/flutter (20407): Tried calling: handler
E/flutter (20407): #0      NoSuchMethodError._throwNew (dart:core/runtime/liberrors_patch.dart:212:5)
E/flutter (20407): [ERROR:flutter/shell/common/engine.cc(178)] Could not run the isolate.
E/flutter (20407): [ERROR:flutter/shell/common/engine.cc(119)] Engine not prepare and launch isolate.
E/flutter (20407): [ERROR:flutter/shell/platform/android/android_shell_holder.cc(167)] Could not launch engine in configuration.

Can you help me please, what I'm doing wrong?

p.s: I'm new to dart and flutter, so maybe I'm doing something really stupid :)

ryanheise commented 5 years ago

The error suggests that you need to use top-level functions. Although it's complaining about handle, I think the problem actually begins with the backgroundTask parameter which the documentation of this plugin states must be a top-level function. See the included example app for how to define this as a top-level function.

So instead of:

backgroundTask: () { .... }

Try this:

backgroundTask: _myBackgroundTask

...

// This must be at the "top level"
// i.e. not nested within any other function or class
void _myBackgroundTask() {
}
ryanheise commented 5 years ago

To address the other part of your question, you can't directly pass any arguments to the background task function, so your background task needs to internally know what it wants to play. However, most apps typically store their state using some form of persistence (shared preferences[1] or a database[2]), so if the current URL is stored in a shared preference, for example, your background task will be able to load that shared preference and fetch the URL.

Future release may provide some alternative options, but this is the way to do it for now.

[1] https://pub.dartlang.org/packages/shared_preferences [2] https://pub.dartlang.org/packages/sqflite

ryanheise commented 5 years ago

I've just published 0.0.10 with the ability to await AudioService.start which completes when the background task is ready to receive messages. This may give you another option to pass data to the background task on start. For example, on the client side:

bool success = await AudioService.start(...);
if (success) {
  AudioService.customAction('url', url);
}

And in the background task:

Completer urlCompleter = Completer();
AudioServiceBackground.run(
   ...
  onCustomAction: (String name, dynamic arguments) {
    switch (name) {
      case "url":
        String url = arguments;
        ...
        break;
    },
  }
);
hacker1024 commented 5 years ago

I need to pass multiple objects into my audioTask. I tried using top-level variables, but they're null in the isolate. To my understanding, I can only pass primitive types in AudioService.customAction.

How can I do this?

ryanheise commented 5 years ago

The main isolate in which your UI runs and the background isolate that is spawned by the audio service do not share any memory, so top-level variables will not be the same between isolates. You have to use one of the various message passing facilities and typically that involves encoding your data into primitives or lists and maps consisting of primitives. If you want to pass whole objects without encoding them, you may try send ports and receive ports via the IsolateNameServer API.

Using the customAction approach, you just need to send your data as a data structure consisting of only lists, maps and primitives. If you already have the data stored in persistence, then you can simplify the amount of data you need to communicate by just sending the keys and on the receiving end use those keys to lookup the full records in your persistence container.

In many cases, you won't need to pass anything since everything will be in persistence. For example, an MP3 player with a playlist and a current track will surely persist this data so that the playlist and current track is remembered on the next startup, and so when the background task starts within its own isolate, it can simply pull this data out of persistence and play it on its own.

hacker1024 commented 5 years ago

If you want to pass whole objects without encoding them, you may try send ports and receive ports via the IsolateNameServer API.

This works beautifully, thanks. It's work noting that this method won't work with dart2js, so my app won't run on Flutter for web when it's available.

ryanheise commented 5 years ago

Great! I'll close this issue.

akdasa commented 5 years ago

@ryanheise, thanks a lot.

Mahfoud047 commented 4 years ago

@ryanheise please can you give as a complete example of using customAction, I tried the code bellow but the player stops playing audio abruptly after few seconds.

void _backgroundAudioPlayerTask() async {
  String url = '';

  CustomAudioPlayer player = CustomAudioPlayer(url); // changed the code inside CustomAudioPlayer
  AudioServiceBackground.run(
    onCustomAction: (String name, dynamic arguments) {
      switch (name) {
        case "url":
          url = arguments;
          break;
      }
    },
    onStart: player.run,
    onPlay: player.play,
    onPause: player.pause,
    onStop: player.stop,
    onClick: (MediaButton button) => player.playPause(),
  );
}

Otherwise how can I use methods like AudioService.addQueueItem() and AudioService.currentMediaItem to pass the MediaItem to the backgroundAudioPlayerTask and play audio from url which is the id of the MediaItem. And in this case I will not need the customAction.

Please give us a complete working example.

themobilecoder commented 4 years ago

The main isolate in which your UI runs and the background isolate that is spawned by the audio service do not share any memory, so top-level variables will not be the same between isolates. You have to use one of the various message passing facilities and typically that involves encoding your data into primitives or lists and maps consisting of primitives. If you want to pass whole objects without encoding them, you may try send ports and receive ports via the IsolateNameServer API.

Using the customAction approach, you just need to send your data as a data structure consisting of only lists, maps and primitives. If you already have the data stored in persistence, then you can simplify the amount of data you need to communicate by just sending the keys and on the receiving end use those keys to lookup the full records in your persistence container.

In many cases, you won't need to pass anything since everything will be in persistence. For example, an MP3 player with a playlist and a current track will surely persist this data so that the playlist and current track is remembered on the next startup, and so when the background task starts within its own isolate, it can simply pull this data out of persistence and play it on its own.

Hi Ryan, Thanks for creating this library. At the moment I'm still trying to understand how to integrate your library with my project because I'm having problems creating my own media notification for both iOS and Android, and yours has already solved it!

However, I disagree with the thing where passing arguments is not really necessary for most cases. I guess for local audio files, but not remotely.

I am trying to build a radio app and it seems a bit hacky when, for example a user selects a Radio station and the app needs to save the URL first(shared_pref/database) just so the background task can read it before playing it. I do understand that it's a technical limitation due to the nature of it being a background task and has to be a top level function, but there must be a better way to pass data (in this case, the URL to play)

For now, I will use the customAction API that you have provided.

Thanks!

ryanheise commented 4 years ago

The persistence approach would make more sense for a podcast or music player which maintains a playlist that is saved locally on the device, and I agree with you that in a radio app where you don't persist playlists, passing arguments at runtime with customAction makes more sense. Instead of customAction, you could also consider using playFromMediaId with the URL as the parameter, if that's the only parameter you need.

themobilecoder commented 4 years ago

The persistence approach would make more sense for a podcast or music player which maintains a playlist that is saved locally on the device, and I agree with you that in a radio app where you don't persist playlists, passing arguments at runtime with customAction makes more sense. Instead of customAction, you could also consider using playFromMediaId with the URL as the parameter, if that's the only parameter you need.

Thanks Ryan. I haven't really noticed the playFromMediaId

Does that mean that if a user wants to play another radio station when another station is already playing, then I can use addQueueItem() and programmatically skip to the next queue then use playFromMediaId() ?

If I understand it correctly, then there's actually no need to pass any arguments and instead just use the queue.

volgin commented 4 years ago

From my perspective, the biggest challenge is not the communication between the isolates, but what to do when the main isolate is killed. The background will continue to play the current item, or even a queue of items, but what happens at the end of a queue? The music simply stops playing until a user revives the app and clicks on something else to play. Is this the correct understanding?

ryanheise commented 4 years ago

Thanks Ryan. I haven't really noticed the playFromMediaId

Does that mean that if a user wants to play another radio station when another station is already playing, then I can use addQueueItem() and programmatically skip to the next queue then use playFromMediaId() ?

If I understand it correctly, then there's actually no need to pass any arguments and instead just use the queue.

I don't think the concept of a "queue" really maps onto a radio app since items in the queue don't have a finite duration (although you're free to interpret the queue how you like). But while listening to one radio station, you can issue another playFromMediaId request with the new URL and implement your callback to stop playing the previous station and switch to the new station.

ryanheise commented 4 years ago

From my perspective, the biggest challenge is not the communication between the isolates, but what to do when the main isolate is killed. The background will continue to play the current item, or even a queue of items, but what happens at the end of a queue? The music simply stops playing until a user revives the app and clicks on something else to play. Is this the correct understanding?

On reaching the end of the queue, you have implementation freedom to decide whether to keep the service alive (e.g. switch to the paused state), or complete the future returned by onStart thereby shutting down the service.

On Android, there is a third option that would be nice to support which I haven't yet, which is the option to shut down the service, but keep the notification alive, giving the illusion that your app is still alive. In this state, the service is not in fact running, and not keeping the CPU alive so your phone can truly sleep if it wants, but if the user opens the notification and presses the play button, the service gets restarted. This is a very battery-efficient option, though it would be a little more complicated to implement this within the plugin so it's an idea for further down the line.

moda20 commented 4 years ago

Sorry to just jump here like this, but if I don't have an already isolated way of moving/storing data ( audio files) I won't be able to use the backgroundTask implementation and therefor the library.

in my case I have a singleton object that holds all my audiophiles that I want to read and It happens that I can't import it to the background task even with DI libraries like GetIt. How do you suggest fixing this ? without having to rewrite everything to be inside the backgroundTask.

ryanheise commented 4 years ago

@moda20 You can pass data between the isolates using custom actions, or standard send/receive ports (search past issues for a discussion).

moda20 commented 4 years ago

@ryanheise I am trying to pass dependency but it keeps getting flagged as invalid argument type. can't we pass objects, pointers to objects in a customAction ?

ryanheise commented 4 years ago

Custom actions pass data over method channels and are limited by the standard codec which supports simple data types. For anything more complex, you need to use send/receive ports (search past issues for a discussion, in particular IsolateNameServer). Note that either way, isolates do not share memory and what is being passed is being passed by value, not by reference.

github-actions[bot] commented 2 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.