ryanheise / audio_service

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

Missing documentation on working with queue #94

Open secretwpn opened 4 years ago

secretwpn commented 4 years ago

It is not very easy to figure out how to correctly work with the queue. Example code doesn't cover this, neither does the existing documentation. Shall we create our own List<MediaItem> myQueue within MyBackgroundPlayer and override all queue related methods to operate myQueue? How do we then tie this to AudioService.queue? Shall we instead update AudioService.queue object from MyBackgroundPlayer queue method overrides? etc. Thanks!

rohansohonee commented 4 years ago

You can use AudioServiceBackground.setQueue to perform queue operations.

alexjuniodev commented 4 years ago

@secretwpn Did you figure out how to work with queue?

secretwpn commented 4 years ago

@rohansohonee this does not explain anything nor does it answer any of the raised questions.

@alexjuniodev yes, sort of. At least I have my take on it, not sure if it is the correct one. What I've ended up doing is overriding all queue related methods in my BackgroundAudioPlayer class (this is the one that extends the library's BackgroundAudioTask)

And I've basically just created my own List<MediaItem> _queue to hold queue items and int _queueIndex to determine my position in the queue. I guess the idea is clear, right? In onAddQueueItem you could just add an item to the _queue In onSkipToNext you manipulate the _queueIndex And whenever you actually setMediaItem - you take the one from the queue like _queue[_queueIndex] and so on.

One thing I don't quite get yet, how AudioService.queue now relates to my own queue. I guess it doesn't, which makes me think that the lib authors had a bit different idea in mind when creating the lib. Well, that's exactly why I was asking for some docs.

rohansohonee commented 4 years ago

Hi @secretwpn

AudioService.queue is to be used to retrieve your queue in UI. AudioServiceBackground.setQueue is to be used to set your queue in the background.

The AudioService class is only for UI code.

rohansohonee commented 4 years ago

Hi @secretwpn

You should not use AudioService class in your BackgroundAudioPlayer class. Your queue logic should reside in BackgroundAudioPlayer class.

The lib author @ryanheise has created a client-server architecture. I hope this is useful.

secretwpn commented 4 years ago

@rohansohonee yea, thanks. I was actually not using AudioService in the background class

The thing that got me puzzled is that in BackgroundAudioTask there is no queue object, so if you override some queue related methods - which list/set/array do you work with? This has initially put me on the way of having my own list as a _queue and then working with it in overrides.

However, in my case, things are even more complicated, due to some limitations my _queue is not even a list of MediaItems but instead a list of objects of my own class that are then converted to MediaItem just before playing. So @alexjuniodev I might be a wrong guy to explain the correct way of using the queue.

ryanheise commented 4 years ago

Thanks everyone for the helpful comments. @rohansohonee 's answer is correct and I would just add that audio_service doesn't implement the queue logic, but it also doesn't implement the audio playing logic, the fast forwarding logic, or any of the logic for that matter.

All of the methods on AudioService simply pass through to the background isolate but nothing will happen unless you override the corresponding methods on the "server side" to implement whatever logic you need.

The architecture is rather like the MediaBrowserService and MediaBrowser Android APIs because that is what this package delegates to under the hood.

It is completely fine to use your own internal representation for your queue. When transmitting queue information through the client/server channel, you need to convert it to a standard representation that can be broadcast not only to your flutter view but also to other clients such as Google Auto. However, each queue item (a MediaItem) contains an ID which you can use as a key into your own queue data structure if you have one.

moritz-weber commented 4 years ago

I've been thinking about this for a few hours now and can't really find a satisfying solution. I think, I get the general idea behind this architecture, but getting information other than MediaItems seems to be overly difficult (I might be missing a major point here though).

My concrete problem is that the audioplayer in the background task needs a url to play a song, but MediaItem doesn't have one. So I'd need some sort of map there to get from ids to urls.

Is there a best practice to solve this issue? Making an extended example with this would be a great help I think.

Edit: Would it be suitable to just add an optional String url property to MediaItem?

ryanheise commented 4 years ago

Hi @moritz-weber

There is actually no rule against using the song's URL as your media ID which would make having a map redundant in your case.

I'll see about expanding on the example.

ryanheise commented 4 years ago

@moritz-weber I've just updated the example to demonstrate queue management. Let me know if that answers your question or if you have any more questions.

yringler commented 4 years ago

Thanks everyone for the helpful comments. @rohansohonee 's answer is correct and I would just add that audio_service doesn't implement the queue logic, but it also doesn't implement the audio playing logic, the fast forwarding logic, or any of the logic for that matter.

All of the methods on AudioService simply pass through to the background isolate but nothing will happen unless you override the corresponding methods on the "server side" to implement whatever logic you need.

The architecture is rather like the MediaBrowserService and MediaBrowser Android APIs because that is what this package delegates to under the hood.

It is completely fine to use your own internal representation for your queue. When transmitting queue information through the client/server channel, you need to convert it to a standard representation that can be broadcast not only to your flutter view but also to other clients such as Google Auto. However, each queue item (a MediaItem) contains an ID which you can use as a key into your own queue data structure if you have one.

@ryanheise, this really clarified for me how this plugin is supposed to be used. I think that this information should be added to the README

ryanheise commented 4 years ago

@yringler agreed, and I'll keep this issue open until I've written more documentation.

xyzbilal commented 3 years ago

when I add AudioServiceBackground.setQueue I am getting error. my function to get dynamic list from api ;

 await RadioProvider().getRadios().then((list){

      if(list.length>0){

        otherRadioList.addAll(list);
        latestItem.value =list[0];

      }

  startService();
});

my function to start service

RxList<MediaItem> otherRadioList = RxList<MediaItem>([]);

  Rx<MediaItem> latestItem =   Rx<MediaItem> ();
  startService([Function function]) async {

     AudioService.start(
            backgroundTaskEntrypoint: audioPlayerTaskEntrypoint,
            androidNotificationChannelName: 'hayirliradyo',
            androidStopForegroundOnPause: true,
            androidNotificationColor: 0xFF2196f3,
            androidNotificationIcon: 'mipmap/ic_launcher_white',
            androidEnableQueue: true,
          ).then((value){

            AudioServiceBackground.setQueue(otherRadioList.toList());

             loadingString.value = 'tamam';
            if(function!=null){

            function();
            }

           // else
           // AudioService.play();

          } );

  }

I am getting error below

E/flutter (28259): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: NoSuchMethodError: The method 'invokeMethod' was called on null.
E/flutter (28259): Receiver: null
E/flutter (28259): Tried calling: invokeMethod<dynamic>("setQueue", Instance(length:7) of '_GrowableList')
E/flutter (28259): #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5)
E/flutter (28259): #1      AudioServiceBackground.setQueue
package:audio_service/audio_service.dart:1538
E/flutter (28259): #2      startService.<anonymous closure>
package:hayirliradyo/utils/audio_services.dart:32
E/flutter (28259): #3      _rootRunUnary (dart:async/zone.dart:1198:47)
E/flutter (28259): #4      _CustomZone.runUnary (dart:async/zone.dart:1100:19)
E/flutter (28259): #5      _FutureListener.handleValue (dart:async/future_impl.dart:143:18)
E/flutter (28259): #6      Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:696:45)
E/flutter (28259): #7      Future._propagateToListeners (dart:async/future_impl.dart:725:32)
E/flutter (28259): #8      Future._completeWithValue (dart:async/future_impl.dart:529:5)
E/flutter (28259): #9      _AsyncAwaitCompleter.complete (dart:async-patch/async_patch.dart:40:15)
E/flutter (28259): #10     _completeOnAsyncReturn (dart:async-patch/async_patch.dart:311:13)

which means ``` _backgroundChannel = const MethodChannel('ryanheise.com/audioServiceBackground');



my question is in which step I should call `AudioServiceBackground.setQueue()` method ? I am using example app.  thanks
ryanheise commented 3 years ago

@xyzbilal , to quote @rohansohonee above, this library uses a client/server architecture. As per the README, you shouldn't actually use the AudioService.* and AudioServiceBackground.* APIs together in the same block of code or even the same isolate. See this paragraph:

Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the AudioService API to communicate with the background task, while your background task will only use the AudioServiceBackground API to interact with the UI and other clients.

The documentation for AudioServiceBackground also says:

Background API to be used by your background audio task.

If you look in the example, you will see that methods in AudioServiceBackground are only ever called from the background audio task and they are typically used to communicate information to the clients (because this is from the "server's" perspective using the client/server analogy). The documentation of setQueue says:

Sets the current queue and notifies all clients.

So that means it should not be used FROM the client to talk to the background audio task, but the other way around. The AudioService.* API is the one that you should use from the client to talk to the background audio task.

Perhaps the documentation was unclear? Which of the above documentation did you find was the most confusing or unclear?

xyzbilal commented 3 years ago

Alright. I am not so much familiar with isolates or backround services and I could'nt get the whole point of what written before but, I solved my problem with logic below.

  1. I use Getx and Get storage for state management and saving preferences. and I have two variable that list for ui and determine the latest mediaitem to hold latest played item and if I stop and start again not to start from 0 index of queue
  RxList<MediaItem> otherRadioList = RxList<MediaItem>([]);
  Rx<MediaItem> latestItem =   Rx<MediaItem> ();
  1. when My Widget is build GetX controller class initilaze and in onStart I call startServices()method then I add AudioService.queue to otherRadioList and I guess the magic trick is AudioService.queue. it immediately gives the queue items when service start and it makes me uptade ui accordingly.
 void startService() async {
   AudioService.start(
            backgroundTaskEntrypoint: audioPlayerTaskEntrypoint,
            androidNotificationChannelName: 'hayirliradyo',
            androidStopForegroundOnPause: true,
            androidNotificationColor: 0xFF2196f3,
            androidNotificationIcon: 'mipmap/ic_launcher_white',
            androidEnableQueue: true,
          ).then((value){

              otherRadioList.addAll(AudioService.queue);
             loadingString.value = 'ok';
          } );

  }

here I listen otherRadioList and I set latest Item if it is saved to preferences.

ever(otherRadioList, (s) {
      durations.clear();
      otherRadioList.forEach((element) {
        final dur = Rx<Duration>();
        durations.add(dur);
      });

      if (box != null && box.hasData('index') && box.read('index') != -1) {
        final index = box.read('index');

        //AudioService.skipToQueueItem(otherRadioList[index].id);
        latestItem.value = otherRadioList[index];
        AudioService.playFromMediaId(latestItem.value.id);
      } else {
        latestItem.value = otherRadioList[0];
        box.write('index', 0);
        AudioService.playFromMediaId(latestItem.value.id);
      }
    });

then I moved my network request to update AudioServiceBackground.queue onStart method of BackroundTask and it worked as I expected. I put here that can be reference for anyone needed .

.....
.....
@override
  Future<void> onStart(Map<String, dynamic> params) async {

    if(queue == null)
       queue = await RadioProvider().getRadios();

    _player.currentIndexStream.listen((index) {

      if (index != null)
      AudioServiceBackground.setMediaItem(queue[index]);

    });
.....
.....
}
nt4f04uNd commented 3 years ago

@ryanheise is this issue still valid for the one-isolate? I believe currently we have it documented and shown in example

ryanheise commented 3 years ago

Technically it is documented now although I would like to polish it further.