Which API doesn't behave as documented, and how does it misbehave?
AudioPlayer.setAudioSource throws a PlatformException when called for a second time on the same instance.
Minimal reproduction project
https://github.com/jimdrie/just_audio/tree/platformexception
To Reproduce (i.e. user steps, not code)
Just run the app.
Error messages
E/flutter (15265): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: PlatformException(Platform player 8cb127f7-d981-4fe9-bc7e-8a6713eb2a7c already exists, null, null, null)
E/flutter (15265): #0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:582:7)
E/flutter (15265): #1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:159:18)
E/flutter (15265): <asynchronous suspension>
E/flutter (15265): #2 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:332:12)
E/flutter (15265): #3 MethodChannelJustAudio.init (package:just_audio_platform_interface/method_channel_just_audio.dart:13:24)
E/flutter (15265): #4 AudioPlayer._setPlatformActive (package:just_audio/just_audio.dart:885:38)
E/flutter (15265): #5 AudioPlayer.load (package:just_audio/just_audio.dart:574:18)
E/flutter (15265): #6 AudioPlayer.setAudioSource (package:just_audio/just_audio.dart:554:24)
E/flutter (15265): #7 _MyAppState._init (package:just_audio_example/main.dart:69:21)
E/flutter (15265): #8 _rootRunUnary (dart:async/zone.dart:1198:47)
E/flutter (15265): #9 _CustomZone.runUnary (dart:async/zone.dart:1100:19)
E/flutter (15265): #10 _FutureListener.handleValue (dart:async/future_impl.dart:143:18)
E/flutter (15265): #11 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:696:45)
E/flutter (15265): #12 Future._propagateToListeners (dart:async/future_impl.dart:725:32)
E/flutter (15265): #13 Future._completeWithValue (dart:async/future_impl.dart:529:5)
E/flutter (15265): #14 _AsyncAwaitCompleter.complete (dart:async-patch/async_patch.dart:40:15)
E/flutter (15265): #15 _completeOnAsyncReturn (dart:async-patch/async_patch.dart:311:13)
E/flutter (15265): #16 AudioPlayer.setAudioSource (package:just_audio/just_audio.dart)
E/flutter (15265): #17 _rootRunUnary (dart:async/zone.dart:1198:47)
E/flutter (15265): #18 _CustomZone.runUnary (dart:async/zone.dart:1100:19)
E/flutter (15265): #19 _FutureListener.handleValue (dart:async/future_impl.dart:143:18)
E/flutter (15265): #20 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:696:45)
E/flutter (15265): #21 Future._propagateToListeners (dart:async/future_impl.dart:725:32)
E/flutter (15265): #22 Future._completeWithValue (dart:async/future_impl.dart:529:5)
E/flutter (15265): #23 _AsyncAwaitCompleter.complete (dart:async-patch/async_patch.dart:40:15)
E/flutter (15265): #24 _completeOnAsyncReturn (dart:async-patch/async_patch.dart:311:13)
E/flutter (15265): #25 AudioPlayer.load (package:just_audio/just_audio.dart)
E/flutter (15265): #26 _rootRunUnary (dart:async/zone.dart:1198:47)
E/flutter (15265): #27 _CustomZone.runUnary (dart:async/zone.dart:1100:19)
E/flutter (15265): #28 _FutureListener.handleValue (dart:async/future_impl.dart:143:18)
E/flutter (15265): #29 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:696:45)
E/flutter (15265): #30 Future._propagateToListeners (dart:async/future_impl.dart:725:32)
E/flutter (15265): #31 Future._completeWithValue (dart:async/future_impl.dart:529:5)
E/flutter (15265): #32 Future._asyncCompleteWithValue.<anonymous closure> (dart:async/future_impl.dart:567:7)
E/flutter (15265): #33 _rootRun (dart:async/zone.dart:1190:13)
E/flutter (15265): #34 _CustomZone.run (dart:async/zone.dart:1093:19)
E/flutter (15265): #35 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
E/flutter (15265): #36 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
E/flutter (15265): #37 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
E/flutter (15265): #38 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
Expected behavior
Load the new audio source without any issues, like it did on 0.5.7.
Smartphone (please complete the following information):
Flutter SDK version
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 1.22.5, on Linux, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[✓] Android Studio
[✓] Connected device (3 available)
• No issues found!
Additional context
It also happens when it's not called in succession, i.e. first playing something and loading a new source halfway.
Thanks for reporting, @jimdrie . I'm investigating now.
Future<Duration> setAudioSource(
AudioSource source, {
bool preload = true,
int initialIndex,
Duration initialPosition,
}) async {
if (_disposed) return null;
// Idea: always keep the idle player around and make it possible
// to switch between idle and active players without disposing either
// one.
_audioSource = null;
_initialSeekValues =
_InitialSeekValues(position: initialPosition, index: initialIndex);
_playbackEventSubject.add(_playbackEvent = PlaybackEvent(
currentIndex: initialIndex, updatePosition: initialPosition));
_broadcastSequence();
// If the active platform existed, we should try to retain it.
_setPlatformActive(false);
_audioSource = source;
_broadcastSequence();
Duration duration;
if (preload) {
duration = await load();
}
return duration;
}
FYI, a quick workaround may be to remove this line:
_setPlatformActive(false);
Although that's not the proper solution I had originally planned. I'll work on a proper fix.
I've just implemented a fix that is somewhat close to what I had originally intended, although some more work can be done to optimise it further. Let me know how it goes and I'll push out a bug fix release.
Not OP, but looking good so far. Thanks for the quick turnaround!
Thank you, that seems to have resolved throwing the exception.
There are still some weird things going on with setAudioSource though. Unless you explicity run await _player.stop(); before setting a new audio source when the player is playing, it won't work. It will look as if it has loaded the source in the user interface (event streams happen) and it is playing (seekbar progresses), but no audio will play and it shows no errors or exceptions in the log. When tapping pause and play again the track will start (from the beginning). In version 0.5.7 calling setAudioSource did not show these issues, and I did not see any documentation about needing to call await _player.stop(); before loading a new source.
Here are the four cases I tried (in the _init function from the example):
This works:
await _player.setAudioSource(_playlist, initialIndex: 2);
_player.play();
await Future.delayed(const Duration(seconds: 2));
await _player.stop();
await _player.setAudioSource(_playlist.sequence[3]);
_player.play();
This works too:
await _player.setAudioSource(_playlist, initialIndex: 2);
_player.play();
await Future.delayed(const Duration(seconds: 2));
await _player.setAudioSource(_playlist.sequence[3]);
await _player.pause();
_player.play();
This doesn't:
await _player.setAudioSource(_playlist, initialIndex: 2);
_player.play();
await Future.delayed(const Duration(seconds: 2));
await _player.setAudioSource(_playlist.sequence[3]);
And this doesn't (because internally it has the status "playing" already):
await _player.setAudioSource(_playlist, initialIndex: 2);
_player.play();
await Future.delayed(const Duration(seconds: 2));
await _player.setAudioSource(_playlist.sequence[3]);
_player.play();
Thank you for your quick response and the great project.
Thanks, @jimdrie . I would definitely want to preserve 0.5.7 behaviour in this new version, and I will take a look into this after dinner. It is also extremely helpful that you have shared these specific test scenarios where the behaviour was not as before.
This behaviour should be fixed in the latest commit.
Tested it for a bit, seems that it's all working fine again in the latest commit, thank you!
Uncertain if this is related, but it seems like it might be a remaining instance of the problem @jimdrie mentions above: I've noticed that if I set preload to false when using setAudioSource, that I have a similar issue - calling play() causes the progress indicator to proceed and it seems like it is playing but there is no sound, but only the first time - seeking to other tracks and calling play seems fine.
Edit It may also be my ignorance of how this works. Calling await player.load() before player.play() seems to fix it.
@pschuegr according to the comments in the file, play() should load the track itself without needing to call load() explicitly, so it is a bug, yes.
/// By default, this method will immediately start loading audio and return
/// its duration as soon as it is known, or `null` if that information is
/// unavailable. Set [preload] to `false` if you would prefer to delay loading
/// until some later point, either via an explicit call to [load] or
/// implicitly on demand when you use a method such as [play] which requires
/// audio to be loaded. If [preload] is `false`, a `null` duration will be
/// returned.
@pschuegr Can you clarify in which order you're calling the methods, and which platform you are testing?
This is certainly an interesting case, because I'm not sure if the ideal behaviour has been settled in my own head. I can change the above comment, or I can change the code, but it seems that setting preload to false while playing==true reveals a contradiction in the doc comments. What should be the expected behaviour in the following situation?
_player.play();
await _player.setAudioSource(..., preload: false);
Clearly, the doc comment above is wrong in that play should NOT require audio to be loaded immediately. The intention was that you CAN call play before an audio source is set and it simply sets a flag that it will play when the processing state is ready. To me, it makes sense that after the above scenario, the player should be in a state where it wants to play but the audio is not ready yet (until you explicitly call load) - which is what happened when I tested it now on Android - so that's good.
On the other hand, what about this?
await _player.setAudioSource(..., preload: false);
_player.play();
Currently play is coded to automatically load the audio and start playing, but perhaps this is wrong. If the app has decided to take loading into its own hands, I now think the above scenario should end up in the same final state as the previous scenario: with playing == true but no actual playing until you explicitly call _player.load(). Certainly the position indicator should not change until the processing state is ready, though (i.e. after the load is successful.)
I have not tested this on iOS yet since I was in the middle of working on the gapless looping code, but on Android where I did test the above two scenarios, I was not able to see the behaviour @pschuegr described:
calling play() causes the progress indicator to proceed and it seems like it is playing but there is no sound
The latest commit makes it so that when you pass preload: false, the audio will not be loaded except via an explicit call to load, and the doc comment is updates as follows:
/// By default, this method will immediately start loading audio and return
/// its duration as soon as it is known, or `null` if that information is
/// unavailable. Set [preload] to `false` if you would prefer to delay loading
/// until some later point, via an explicit call to [load]. If [preload] is
/// `false`, a `null` duration will be returned.
I am not sure if this helps with the reported issue, it would be a pleasant surprise if that is resolved. But let me know if you notice further issues, and how to reproduce them. Once again, thanks for helping to discover these issues, and all of these test cases will likely become automated test cases in the future.
I'm testing this on MacOS and I can verify that it seems to behave as expected (ie I don't need a call to load) on Android for me as well, so it's possible this is a platform-specific issue?
If I get time this week I'll see if I can reproduce this minimally, it'll take a while to extract it from my other code right now.
Thoughts on your comment above:
My read on this initially was that "preload" meant "load data for the first track now", not "do load automatically on a call to play", which seems to be how you are thinking about it? Now that I understand better, I think that if that's the way you want to go with it, it might be nice to have an optional parameter to play(bool load: false). I suspect the vast majority of times the desired behaviour when calling play is to load if necessary.
I'm testing this on MacOS and I can verify that it seems to behave as expected (ie I don't need a call to
load) on Android for me as well, so it's possible this is a platform-specific issue?
I'm not yet completely clear on a few things which I'll need to understand further:
play before or after setAudioSource and whether you're using preload: false)Thoughts on your comment above:
My read on this initially was that "preload" meant "load data for the first track now",
Your initial read is correct.
not "do
loadautomatically on a call toplay",
Right, not this. To give some context, play is equivalent to the ExoPlayer model where it essentially means "play when ready". If you manually take loading into your own hands, the audio will not be ready until you load it.
it might be nice to have an optional parameter to
play(bool load: false). I suspect the vast majority of times the desired behaviour when calling play is to load if necessary.
In other words (correct me if I'm misunderstanding), play(true) would be a convenience method for calling load then play, since load will be a no-op if already loaded. I can see how that would be convenient, although if setAudioSource hadn't been called previously, what should the load parameter do?
Sorry, re-reading I realize my comment wasn't that clear:
setAudioSource with preload: false and then play. preload: false means "don't preload the first track data" (as you confirmed) but that play() called after that would also imply a load if necessary, ie preload refers only to the initial track when calling setAudioSource.load:true is meaningless. But I find the idea of saying that the player is "playing" without something to play a bit confusing to start with, although from an implementation POV I see why it would make sense. I'm not familiar with ExoPlayer, TBH - is this is the underlying Android player? but I don't entirely follow the use case for this order that you mentioned above:
_player.play();
await _player.setAudioSource(..., preload: false);
Is this when you want to start playing an audio source, but then you later want to change the audio source but retain the playing state? In this scenario, preload seems like it would be basically meaningless, since attempting to play from the new source requires loading, so whether you trigger it as a) part of that call or b) through whatever internal mechanism would decide to play the track and then have to go load it seems moot.
[Please take everything I say with a grain of salt, I'm still wrapping my head around just_audio and audio_service]
Sorry, re-reading I realize my comment wasn't that clear:
I'm calling
setAudioSourcewithpreload: falseand thenplay.This works as expected on Android, but on MacOS it has the previously described behaviour (seems to play but no sound).
Thanks for clarifying, I'll stash my other iOS code and do some testing on this.
- Ultimately I'm happy to use the API as you've presented it in the last comment, but the behaviour that would match my intuition would be that
preload: falsemeans "don't preload the first track data" (as you confirmed) but thatplay()called after that would also imply a load if necessary, iepreloadrefers only to the initial track when callingsetAudioSource.- To your last question, in the case that you have no source to play, I guess load:true is meaningless. But I find the idea of saying that the player is "playing" without something to play a bit confusing to start with, although from an implementation POV I see why it would make sense.
I'm not familiar with ExoPlayer, TBH - is this is the underlying Android player?
I think I shared your initial intuition when first learning ExoPlayer's state model. ExoPlayer's model seemed counter-intuitive to me, and in an earlier version of this plugin, I fought against ExoPlayer's model and exposed a more traditional API over the top of it. However, I eventually ran into more and more use cases where I needed the flexibility of ExoPlayer's model and started to appreciate that it was superior. You can, after all, still use this API to emulate all of the traditional use cases, but you can also capture richer state information, and eliminate delays in the decoding pipeline. In addition to that, there is a certain simplicity to it that fits into their threading model, where the decoder pipeline runs on a background thread, and the main API runs on the main thread. The playing/paused state transitions happen entirely on the main thread, while the processing state transitions happen entirely on the background thread (at least in the original ExoPlayer, anyway). So to put it another way, the value of playing only ever changes when "you" set it to true or false. You will never find the background thread automatically switching its value. On the other hand, the processing state will change according to the state of the decoder, e.g. when buffering transitions between empty or filled, when audio is loaded or not, etc. ExoPlayer names the playing state as "play when ready" which is more accurate. It indicates the desire to play as soon as the decoder buffers are filled, and does not indicate that audio is actually playing right now. So if you lose network connectivity, the audio isn't technically playing, but the player remains in this "play when ready" mode, and your play/pause button should still be in the "playing" state.
One other example is that if you take a look at the YouTube app, the playing state only ever changes when the user toggles the play/pause button. When reaching the end of the video, the video is still in "playing" mode, it's just that there is no audio in the buffers ready to play. If you click to an earlier point on the seek bar, you will find that it continues playing audio, and it is not because the player automatically transitioned from "pause" into "playing" again, but rather that the player had remained in the "playing" state the whole time.
It may be that by splitting just_audio 0.5.7's load method into 2 steps has made it more difficult to get an intuition for this than before. I completely understand that, and perhaps it would be clearer if I went "full" ExoPlayer and removed the play/pause methods and instead had a setPlayWhenReady(bool) method.
Then, in this scenario, we can understand it as follows:
await player.setPlayWhenReady(true); // not playing yet because not ready
await player.setAudioSource(..., preload: false); // not playing yet because not ready
await player.load(); // audio will start playing after the load completes because the processing state has become "ready"
But, I don't have the courage to do that ;-)
but I don't entirely follow the use case for this order that you mentioned above:
_player.play(); await _player.setAudioSource(..., preload: false);Is this when you want to start playing an audio source, but then you later want to change the audio source but retain the playing state? In this scenario, preload seems like it would be basically meaningless, since attempting to play from the new source requires loading, so whether you trigger it as a) part of that call or b) through whatever internal mechanism would decide to play the track and then have to go load it seems moot.
The above scenario is useless on its own, I guess the point of exploring it is to ensure that the state model has well-defined behaviour in all possible states. It would be similar to the uselessness of calling other methods in meaningless orders (e.g. calling pause twice in a row - it is still possible, but not useful, and we just need to ensure that the player can cope with all state transitions.)
However, this scenario is useful:
_player.play();
await _player.setAudioSource(..., preload: true);
The best way to think about it is that the playing state directly corresponds to the state of your play/pause button. In the above code snippet, what this code is saying is that the play button should be in the "playing" state as soon as the user clicks the button, even while the audio source is loading, and I think this would make intuitive sense from the user's perspective. True, the audio isn't "technically" playing while it's loading, but the user has expressed their intention to play audio by hitting the button, and they know that it will play when ready.
It would just be considered a programmer mistake if they just called _player.play() without ever following that by code that would actually make the processor "ready" to play audio.
[Please take everything I say with a grain of salt, I'm still wrapping my head around just_audio and audio_service]
Not at all, I appreciate hearing your view. This is certainly counter intuitive particularly if you're not familiar with ExoPlayer's model. I've also introduced something new into the model by making loading a two stage process, and so there is certainly room for thinking about whether the way I've done it is sensible or whether there is a better way.
I've thought about how to make play() implicitly load the audio in a way that would work in all scenarios and have spec'd out a new play method with the following doc comment:
/// Tells the player to play audio as soon as an audio source is loaded and
/// ready to play. If an audio source has been set but not preloaded, this
/// method will also initiate the loading. The [Future] returned by this
/// method completes when the playback completes or is paused or stopped. If
/// the player is already playing, this method completes immediately.
And for setAudioSource:
/// By default, this method will immediately start loading audio and return
/// its duration as soon as it is known, or `null` if that information is
/// unavailable. Set [preload] to `false` if you would prefer to delay loading
/// until some later point, either via an explicit call to [load] or via a
/// call to [play] which implicitly loads the audio. If [preload] is `false`,
/// a `null` duration will be returned. Note that the [preload] option will
/// automatically be assumed as `true` if `playing` is currently `true`.
What I wanted to achieve was identical behaviour in both of my original scenarios, i.e. calling play alternatively before setAudioSource or after it. The new spec for both methods above should achieve that.
The latest commit implements the above spec.
I think at least in principle, this spec seems right, but of course please do let me know if the implementation of it doesn't live up to the spec.
I am now able to reproduce the issue of the position indicator moving without any sound (on iOS). This means I'll also be able to investigate it and fix it. Unfortunately though it's time to sleep so it will have to wait until the morning (apologies for the delay, but I do expect to be able to resolve this issue tomorrow.)
Thanks for the more detailed explanation of the ExoPlayer model - I can understand that it would be more powerful in some scenarios.
The spec as listed seems great to me 👍 I'll give it a try today. Thanks for all your work on this!
The latest commit should fix the iOS/macOS issue. This commit also includes a change to the Dart play code to return the correct Future when transitioning between inactive/active platforms.
I just need to do some testing on web, but if this gets us close enough to solving the original problem, I would like to push out a release to at least to replace the current release.
More clean up and fixed a bug in the web implementation. I'm not entirely happy with the code in its current state and will want to revisit it at some point, but my goal at the moment is just to get a fix rolled out since we still have a public release that contains the bug.
(The main issue here is to avoid a race condition between simultaneously calling load and play, the current solution being to let the platform side of the plugin handle it.)
Unit tests are updated and all passing.
After some more testing on each platform, things seem to be stable so I've decided to go ahead and release 0.6.2. Let me know how it goes. There may be some remaining undiscovered issues but I thought I should at least publish an incrementally better version sooner rather than later.
0.6.3 is released:
- Fix iOS compiler warnings.
- Fix bug where native platform wasn't disposed.
- Remove Java compiler warnings.
- Process play/pause state transitions in a consistent order.
Haven't found any issues anymore, feel free to close. Thank you!
Glad to hear it! Thanks @jimdrie and @pschuegr for also contributing helpful test cases.
0.6.3 is released:
- Fix iOS compiler warnings.
- Fix bug where native platform wasn't disposed.
- Remove Java compiler warnings.
- Process play/pause state transitions in a consistent order.
_player.stop();
then
_player.play();
_player.setAudioSource(AudioSource.uri(uri));
Will throw this error on just_audio 0.6.3
@wangbo4020 I will try to improve this, but I will try to explain what is happening so that in the meantime your app can deal with it.
The difference between pause and stop is that pause is intended if you expect to resume audio playback again soon, while stop is intended when you are done playing audio for a while. stop powers down everything, and releases the native platform decoders, etc. So if you call play immediately after calling stop, this is usually an anti-pattern, and pause would be more appropriate. That being said, taking all 3 lines of your scenario all together, if you want to stop playing the current audio source and then play the next audio source, you can simply skip the first two lines and just set the new audio source. That will work and it won't need to needlessly spin down everything only to then spin it up.
If you want to spin everything down and then immediately spin it up, you need to wait for it to spin down first. That is, await the call to stop:
await _player.stop();
_player.play();
await _player.setAudioSource(AudioSource.uri(uri));
I suggest instead that you should not spin it down in the first place, and just do this:
await _player.setAudioSource(AudioSource.uri(uri));
As for when to use stop, it would make sense for some apps to have an idle timeout so that if the player is paused for a long enough time, the app will then automatically call stop to release the resources.
If by chance the user happens to press the play button exactly at this moment, you could currently either catch that error and then try again after a second, or, you could keep a reference to the future returned by stop and wait for this future to complete before issuing a play.
With that said, ideally I can handle this for you in the plugin directly, but until then hopefully the above strategies will help you.
@wangbo4020 I will try to improve this, but I will try to explain what is happening so that in the meantime your app can deal with it.
The difference between
pauseandstopis thatpauseis intended if you expect to resume audio playback again soon, whilestopis intended when you are done playing audio for a while.stoppowers down everything, and releases the native platform decoders, etc. So if you callplayimmediately after callingstop, this is usually an anti-pattern, andpausewould be more appropriate. That being said, taking all 3 lines of your scenario all together, if you want to stop playing the current audio source and then play the next audio source, you can simply skip the first two lines and just set the new audio source. That will work and it won't need to needlessly spin down everything only to then spin it up.If you want to spin everything down and then immediately spin it up, you need to wait for it to spin down first. That is, await the call to
stop:await _player.stop(); _player.play(); await _player.setAudioSource(AudioSource.uri(uri));I suggest instead that you should not spin it down in the first place, and just do this:
await _player.setAudioSource(AudioSource.uri(uri));As for when to use
stop, it would make sense for some apps to have an idle timeout so that if the player is paused for a long enough time, the app will then automatically callstopto release the resources.If by chance the user happens to press the play button exactly at this moment, you could currently either catch that error and then try again after a second, or, you could keep a reference to the future returned by
stopand wait for this future to complete before issuing aplay.With that said, ideally I can handle this for you in the plugin directly, but until then hopefully the above strategies will help you.
Thanks very much
Hi @wangbo4020 , the latest commit to master should now gracefully handle rapid successive calls to stop and play/setAudioSource. Essentially, if you don't call await yourself, the plugin will internally do the await for you and queue the requests.
Note: This change affects the same area of code that I was fixing earlier, so I will need to retest everything above to ensure that this hasn't adversely affected anything that was previously fixed.
@wangbo4020 After some testing I have now published this code as 0.6.4. Likely you have already optimised your code in a way that doesn't call stop and thus doesn't run into the issue, but in case you run into the issue again, let me know and I'll re-open this issue.
@wangbo4020 After some testing I have now published this code as 0.6.4. Likely you have already optimised your code in a way that doesn't call
stopand thus doesn't run into the issue, but in case you run into the issue again, let me know and I'll re-open this issue.
Thanks very much again.
I apply 0.6.4, this error has been fixed.
Now, I use it like:
When user play and switch music
_player.setAudioSource(AudioSource.uri(uri));
_player.play();
When user stop music
_player.stop();
Most helpful comment
After some more testing on each platform, things seem to be stable so I've decided to go ahead and release 0.6.2. Let me know how it goes. There may be some remaining undiscovered issues but I thought I should at least publish an incrementally better version sooner rather than later.