Closed agersant closed 1 year ago
This seems related to https://github.com/ryanheise/just_audio/issues/294.
A solution their suggests to keep your own list of playing queue, and insert items on the go.
Initialise a ConcatenatingAudioSource with an initial sublist of children small enough to load without issue, and then write your own application logic to lazily add more children as needed (just-in-time). If you want to create the illusion that the list of children is complete from the beginning, you will need to write your UI around some other more complete list of metadata, separate from ConcatenatingAudioSource's own internal list of items.
But, this is about inserting items slowly and as needed. Your issue is about shuffle, which rearranges all of audio sources. This means you might have to handle shuffle yourselves on your own playing queue. Although, if you go on this path like me, there might be more work you will need to put in to support other features like: next item, previous item, skip to item, shuffle-mode, repeat-mode, etc. Perhaps, move these features outside of the library too.
My playing queue is generally 1000s of songs, but actual audio-sources that are queued in to audio-player are about 5, keeping the current-index at 2.
I have never encountered it though (slowness), but this is a discussion I am interested in as well.
Thanks for chiming in! I did look at this issue too, but I think it is an independent problem.
The slowdown in this one is not related to creating the concatenating audio source but specifically in just_audio_background
code that cares about the shuffling order. It behaves well until shuffle mode is enabled.
My bad, I apologize.
Can you confirm that it happens only with the use of just_audio_background, and works fine with vanilla just_audio?
I just verified, this issue does not happen when using vanilla just_audio
. 😮
Video recording with vanilla just_audio
(this also has a very large concatenating audio source):
https://user-images.githubusercontent.com/817256/201025844-4fb32490-a8c4-4456-a676-eb4d65e5205b.mp4
I just pushed this test in the shuffle
branch of https://github.com/agersant/just_audio. I also just realized I had forgotten to push the initial example using just_audio_background
, but it is now in there too.
Relevant commits:
just_audio
: https://github.com/agersant/just_audio/commit/b32b6159fc9f356f3727991151ed45ffe2689301just_audio_background
: https://github.com/agersant/just_audio/commit/9bc28c8224944ef348efbc109e1339dbbf29d397Something very strange seems to be happening in the FlutterEngine. Normally Dart will execute lines of code continuously until it gets to an await
, which yields to the next coroutine. However, it seems like Dart is actually pausing at a line of code where there is no await
, and nothing intensive is happening on that line.
Without understanding that side of things fully, my guess is that audio_service might be doing some intensive work on the main thread (although it shouldn't be), such as broadcasting this very large queue. I don't actually know if Android was designed to broadcast such a large sized queue.
Another thing you could test to confirm this is modifying the audio_service example in a similar way.
Thank you for investigating ♥
I took a stab at reproducing this with the audio_service
playlist example. The repro code is over there: https://github.com/agersant/audio_service/commit/7294f9072f16712d1c880b73e9d550c9ec338e42
This did not exhibit the large performance degradation either - so it seems the problem is limited to just_audio_background
.
This lines up with my theory that get shuffleIndices
from just_audio_background
is the offending function that takes a lot of time and is called repeatedly.
Hmm, that wouldn't explain how it is that the main Flutter isolate is being interrupted or paused at points where there is no await
. I was thinking that maybe audio_service was also doing something on the main thread (like loading artwork for the media session). In the past this has been a source of issues although I also think that has since been fixed.
It's truly a mystery. I am curious to know what's really happening that could cause an isolate to be interrupted like this, but I suspect it's not going to be easy to track down.
I am also unsure what darkness you peered into within Flutter Engine, but I found a trivial optimization which fixes this problem. It turns out get shuffleIndices
was being called way more times than necessary 🥳
Oh, right! That code is too inefficient. In just_audio core, the inverted indices are cached and recomputed only when necessary in _updateShuffleIndices
, and that same approach should probably also be done in just_audio_background.
There is still a slight delay when pressing the shuffle button, so I also think that shuffle()
should probably also be async, and the default implementation should probably try to do the shuffling in an isolate on platforms where isolates are available.
In just_audio core, the inverted indices are cached and recomputed only when necessary in _updateShuffleIndices, and that same approach should probably also be done in just_audio_background.
I can try to update the PR in this direction. However, I'm not sure when the appropriate time to update the shuffle indices is. Would it be everywhere that _updateQueue()
is called and from customSetShuffleOrder
(after it calls shuffle on the player?)? I'm very unclear on what the correct order of operation is.
so I also think that shuffle() should probably also be async
Which shuffle
method is this about? Is that the one on Audiosource
? 😔 (and does this belong in the same PR?)
In just_audio core, the inverted indices are cached and recomputed only when necessary in _updateShuffleIndices, and that same approach should probably also be done in just_audio_background.
I can try to update the PR in this direction. However, I'm not sure when the appropriate time to update the shuffle indices is. Would it be everywhere that
_updateQueue()
is called and fromcustomSetShuffleOrder
(after it calls shuffle on the player?)? I'm very unclear on what the correct order of operation is.
That sounds reasonable. In terms of the correct order, I think it should be done immediately after queue.add
and after _source = request.audioSourceMessage
.
so I also think that shuffle() should probably also be async
Which
shuffle
method is this about? Is that the one onAudiosource
? 😔 (and does this belong in the same PR?)
That would be separate from this PR, but yes I'm thinking of AudioSource.shuffle
and also the one in ShuffleOrder
itself.
Thanks! I updated the PR with your suggestions. It is working great as far as I can tell (performance and functionality).
One thing that puzzles me is I would have thought the indices need to be recalculated before calling queue.add
. My reasoning was that app UIs could have callbacks firing in response to the queue
broadcast, and these callbacks might read values like nextIndex
that depend on the shuffle indices. I wasn't able to trigger any bug along those lines so my mental model must have been incorrect.
That's a good point. Now, nothing consequential actually listens to the queue
stream here, rather, _broadcastState
is called at appropriate times to update the notification based on hasNext
/hasPrevious
, so it is a matter of ensuring the recalculation is done before calling _broadcastState
. The reason you did not trigger any bug is likely because _broadcastState
is only called in the first of the following methods when it should probably be called in all of them:
Future<SetShuffleOrderResponse> customSetShuffleOrder(
SetShuffleOrderRequest request) async {
_source = request.audioSourceMessage;
_broadcastStateIfActive();
return await (await _player).setShuffleOrder(SetShuffleOrderRequest(
audioSourceMessage: _source!,
));
}
Future<ConcatenatingInsertAllResponse> customConcatenatingInsertAll(
ConcatenatingInsertAllRequest request) async {
final cat = _source!.findCat(request.id)!;
cat.children.insertAll(request.index, request.children);
_updateQueue();
return await (await _player).concatenatingInsertAll(request);
}
Future<ConcatenatingRemoveRangeResponse> customConcatenatingRemoveRange(
ConcatenatingRemoveRangeRequest request) async {
final cat = _source!.findCat(request.id)!;
cat.children.removeRange(request.startIndex, request.endIndex);
_updateQueue();
return await (await _player).concatenatingRemoveRange(request);
}
Future<ConcatenatingMoveResponse> customConcatenatingMove(
ConcatenatingMoveRequest request) async {
final cat = _source!.findCat(request.id)!;
cat.children
.insert(request.newIndex, cat.children.removeAt(request.currentIndex));
_updateQueue();
return await (await _player).concatenatingMove(request);
}
I guess technically it need only be called if the hasNext
/hasPrevious
state has changed. I don't know how much a gain there is from such an optimisation, but to pick one example, you could do this:
Future<ConcatenatingInsertAllResponse> customConcatenatingInsertAll(
ConcatenatingInsertAllRequest request) async {
final hadPrevious = hasPrevious, hadNext = hasNext;
final cat = _source!.findCat(request.id)!;
cat.children.insertAll(request.index, request.children);
_updateShuffleIndices(); // new
if (hasPrevious != hadPrevious || hasNext != hadNext) _broadcastStateIfActive(); // new
_updateQueue(); // and also undo the changes here since we update the indices above
return await (await _player).concatenatingInsertAll(request);
}
Thanks a lot for the clarification, I definitely needed it.
I updated the PR again with the suggested changes, modulo:
hasPrevious
/hasNext
as it looks a little brittle (eg. could cause issues if new correlated fields are added to PlaybackState
), and it felt bad to copy-paste it everywhere. I also don't imagine the performance gain would be significant._updateShuffleIndices
(in setShuffleMode
) so I added that one too.Sorry for the delay, however this is looking good :+1: . I will do some testing and hopefully merge soon.
I found one more case which is the initial case when the audio is first loaded. This is added in my copy of your branch: agersant-shuffle-fix
.
If that works for you, I'd be happy to go ahead and merge/publishg.
Oh nice catch on the initial load. Works for me indeed!
Thanks for the continued hard work ♥
Your pull request is now released on pub.dev as 0.0.1-beta.8 :+1: Thank you for the contribution!
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 just_audio.
Which API doesn't behave as documented, and how does it misbehave? When calling
setShuffleModeEnabled(true)
on an audio player which is currently playing aConcatenatingAudioSource
with several hundreds/thousands of children, all audio player operations like play/pause take multiple seconds to complete while blocking the UI thread.Minimal reproduction project https://github.com/agersant/just_audio/
To Reproduce (i.e. user steps, not code) Steps to reproduce the behavior:
shuffle
branchjust_audio_background\example
using an Android device or emulatorshuffle
buttonError messages
Expected behavior App performance is not significantly degraded when enabling shuffle mode.
Screenshots
Video demonstration: https://user-images.githubusercontent.com/817256/201009999-fbf6b984-ce31-45f5-bed7-6d17c1c03d32.mp4
Desktop (please complete the following information):
Smartphone (please complete the following information):
Flutter SDK version
Additional context Pausing execution during the very long hitches reveals that the time is being spent in
get shuffleIndices
withinjust_audio_background.dart
. Example callstack: