ryanheise / just_audio

Audio Player
1.02k stars 628 forks source link

ConcatenatingAudioSource with lots of children takes a very long time to load #294

Open smkhalsa opened 3 years ago

smkhalsa commented 3 years ago

Which API doesn't behave as documented, and how does it misbehave?

Creating a ConcatenatingAudioSource with lots (~1000) of children takes a very long time (>20 seconds).

In my application, users can have playlists with an arbitrary number of items.

Minimal reproduction project

To Reproduce (i.e. user steps, not code)

final player = AudioPlayer();
final songs = /// 1000+ sources
await player.setAudioSource(
  ConcatenatingAudioSource(children: songs),
);

Error messages

Expected behavior

I'd expect to be able to set the audio source and start playing the first source within a couple seconds, even with a large number of sources (since only the initial few sources are actually being fetched).

Screenshots

Desktop (please complete the following information):

Smartphone (please complete the following information):

Flutter SDK version

[✓] Flutter (Channel beta, 1.25.0-8.3.pre, on Mac OS X 10.15.7 19H114 darwin-x64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.1)
[✓] VS Code (version 1.52.1)
[✓] Connected device (2 available)

• No issues found!

Additional context

NOTE added by @ryanheise :

Adding a large number of children is problematic at multiple levels due to the single-threaded model. For now, there are three approaches to workaround this issue:

  1. Initialise a ConcatenatingAudioSource with zero children, and then add the children in a loop that yields every now and then to let other coroutines execute on the same thread. This can be done for example by periodically calling await Future.delayed(Duration.zero);.
  2. 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.
  3. There is an experimental branch feature/treadmill (now merged) that implements the logic of (2) above but on the iOS native side (Android already has this implemented on the native side when useLazyPreparation is true). This will stop iOS from trying to load every item in the playlist, and instead only load items that are nearer to the front of the queue. If you test this branch, please share below whether you found it stable enough to release.
mz5210 commented 1 year ago

@ryanheise

ryanheise commented 1 year ago

Hi @mz5210 Please write your comments in English, thank you.

mz5210 commented 1 year ago

@ryanheise I tested 502 pieces of music, adding them to the playlist in bulk, it worked fine, no stuck, maybe you should incorporate this change into master

burhanaksendir commented 1 year ago

Hi @ryanheise, Any chance to update feature/treadmill?

ryanheise commented 1 year ago

Just updated.

zhahouming commented 1 year ago

@ryanheise works fine! image

jmshrv commented 1 year ago

First impressions on feature/treadmill - my weird issue with gapless sometimes being broken is fixed! I'll use it on my own device for a bit to see how stable it is :)

jagpreetsethi commented 1 year ago

@ryanheise Would this already address a scenario where audio media needs to be loaded from an HTTP URL which becomes available after making an additional request ?

Something like below: Items loaded in the playlist is When actually playing a music, the item from the above list is HTTP requested, which returns an MP3 audio file to play.

If feature/treadmill doesn't address this, what approach would you recommend ?

Thanks in advance.

ryanheise commented 1 year ago

777 ?

LouisAldorio commented 1 year ago

so I can load 2000 audio now directly to the player without any freezing issue ?

ryanheise commented 1 year ago

@LouisAldorio Try it and let me know how it goes. The treadmill branch has been put out there to be tested in order to receive feedback.

LouisAldorio commented 1 year ago

Branch feature/treadmill right ? , tomorrow I will try it let you know , but just to confirm I am not using the wrong branch , feature/treadmill right , sir ryan

ryanheise commented 1 year ago

but just to confirm I am not using the wrong branch , feature/treadmill right

Well, if you find any other treadmill branch, please let me know about it!

LouisAldorio commented 1 year ago

everything works well, 917 tracks, added and instantly played lol. that was fast

hunterwilhelm commented 11 months ago

Even though mine instantly started playing, it took about 30 seconds to add 1000 tracks in the newest version of feature/treadmill on iOS with all the flags enabled. I see a huge improvement, but it is still not satisfactory for shuffle mode to work properly when starting the app.

ryanheise commented 11 months ago

@hunterwilhelm Can you just clarify, so it started playing BEFORE it added the tracks? In which case, what precisely do you mean by adding the tracks? Do you mean the time it took to appear in the UI?

hunterwilhelm commented 11 months ago

@ryanheise Here is how I have it in the code right now.

  1. My app initializes
  2. Download the URLs of the tracks and start loading them one by one using .add instead of .addAll because .addAll is still freezing the UI.
  3. After the first add, the UI looks fine,
    • but shuffle mode will be very wrong because it won't take the whole playlist into account. Rather, it would only shuffle the songs that have been added so far, so when you hit next, you have an 0% chance of getting the last track in the playlist before it's done. Not a true shuffle - yet.
  4. The user presses play, and it instantly plays because the beginning of the playlist has already loaded.
  5. Then after 30 seconds after step 2, the tracks have all been added.
  6. At this point, theoretically, I can use shuffle again, but it's too late. I would like to have a working shuffle without having to wait.

Right now I am making my own implementation of the dynamically loading so it works on iOS. I am loading the tracks into the playlist one by one when needed. But if the user starts in the middle of the playlist, they need to go backward. So, I am going to have to write a complicated function to map the indices of the queues (shuffled and chronological) to the playlist indices (for precaching purposes) and vice versa.

ryanheise commented 11 months ago

In the case of .addAll or passing all children into the constructor, there are several points in the code that cause the slowdown. The treadmill branch addresses only one of them, which is to prevent the native player on iOS from trying to load the entire playlist all at once. But there are still 2 other bottlenecks. One is passing a large playlist over the flutter method channel all at once, and the other is purely on the dart side when processing long lists. Dart is not multi-threaded, so any time we do a for loop over a long list, that is potentially going to lock up the UI.

If you do have a very long playlist, yes, you do have to just avoid passing very long lists into ConcatenatingAudioSource and instead use it more dynamically so that you maintain your own Dart list containing your entire playlist, but you use ConcatenatingAudioSource to only ever contain the next 3 items to play. When you reach the end of the current track, you remove it from the front and add a new track onto the back so that you always have 3 items queued up for gapless playback.

The long term future direction of the plugin should be to shift the internal implementation somewhat in this direction. There are complications to that, so there is no short term quick fix, apart from using ConcatenatingAudioSource within your app in a more dynamic way as described above.

jagpreetsethi commented 11 months ago

@ryanheise Is there any example code available that I may refer that follows your suggested approach to fill ConcatenatingAudioSource dynamically by 3 items only? Thanks in advance!

hunterwilhelm commented 11 months ago

@ryanheise thanks for that quick response! That makes sense. Someday I'll be knowledgeable enough on the topic to be able to assist with the amazing work you do!

@jagpreetsethi Once I finish my implemention of what Ryan is talking about, I'll post it here. It will include shuffling.

burhanaksendir commented 11 months ago

Hi @ryanheise

I've been trying to include the just_audio package in my Flutter project using its Git repository as the source. However, I've been encountering an issue when specifying the Git reference in the pubspec.yaml file.

Here's how I'm adding the package to my pubspec.yaml file:

just_audio:
  git:
    url: https://github.com/ryanheise/just_audio.git
    ref: feature/treadmill

However, every time I attempt to fetch the package, I get an error indicating that the pubspec.yaml file cannot be found in the specified Git reference:

Could not find a file named "pubspec.yaml" in https://github.com/ryanheise/just_audio.git 02f74512e51c40ba68fe4c898d570c0c4ae906d5.

The reference I'm using is: feature/treadmill, and I've verified that it exists in the repository: https://github.com/ryanheise/just_audio/tree/feature/treadmill.

I've tried cleaning the cache, updating Flutter packages, and even cloning the repository locally to test, but the issue persists.

Is there something I might be missing or any suggestions you can provide to help me resolve this? I greatly appreciate your assistance in this matter.

Thank you!

ryanheise commented 11 months ago

It's because just_audio isn't in the root directory of this repo, it's in the just_audio subdirectory, so you need to specify that with path: just_audio. So:

just_audio:
  git:
    url: https://github.com/ryanheise/just_audio.git
    ref: feature/treadmill
    path: just_audio
Eldar2021 commented 11 months ago

For me this feature/treadmill works good. I would ask you to merge it. Thank you for the nice package and your support.

hunterwilhelm commented 10 months ago

@jagpreetsethi I implemented what @ryanheise was suggesting. However, I wasn't able to figure out shuffling while preloading without having any visual glitches or refreshes when switching between shuffle mode and non-shuffle mode. So I left it out. If anyone can modify it to add it, I would be very grateful.

The Implementation: ```dart import 'dart:async'; import 'dart:math' show max, min; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import 'package:quiver/iterables.dart' show range; import 'package:rxdart/rxdart.dart'; Future initAudioService() async { return await AudioService.init( builder: () => MyAudioHandler(), config: const AudioServiceConfig( androidNotificationChannelId: 'com.example.app.audio', androidNotificationChannelName: 'Example App', androidNotificationOngoing: true, androidStopForegroundOnPause: true, ), ); } class PlaylistManager { final _player = AudioPlayer(); final _subPlaylist = ConcatenatingAudioSource( children: [], useLazyPreparation: false, // I want it to pre-cache everything I put in this playlist. ); /// A copy of the media items so we can access them later final List _subPlaylistItems = []; /// The full list of media items to pull from final List _fullPlaylist = []; /// Used for keeping track of how many tracks are in front of the current index of the player. int _playlistFrontLoadingCount = 0; /// Change this if you want the index to not start at the beginning static const kDefaultIndex = 0; /// Keeps track of the current index of the _fullPlaylist int _currentFullPlaylistIndex = kDefaultIndex; /// Used for cleaning up the subscriptions when disposed final List _streamSubscriptions = []; /// Used for preventing race conditions in the _lazyLoadPlaylist bool _updatePlayerRunning = false; /// Used for preventing race conditions in the _lazyLoadPlaylist bool _updatePlayerWasCalledWhileRunning = false; /// Used for not skipping too many times if the [_subPlaylist] isn't done loading yet. int _directionOfSkipTo = 0; // PUBLIC VARS / GETTERS /// Notifies the system of the current media item final BehaviorSubject mediaItem = BehaviorSubject(); /// Use this to get the live data of what the state is of the player /// /// Don't perform actions like skip on this object because there is extra logic /// attached to the functions in this class. For example, use [seekToNext] instead of [player.seekToNext] AudioPlayer get player => _player; /// The index of the next item to be played int get nextIndex => _getRelativeIndex(1); /// The index of the previous item in play order int get previousIndex => _getRelativeIndex(-1); /// The index of the current index. int get currentIndex => _currentFullPlaylistIndex; /// How many tracks next and previous are loaded into ConcatenatingAudioSource /// * Ex. 3 would look like [`prev3`, `prev2`, `prev1`, `current`, `next1`, `next2`, `next3`] final int preloadPaddingCount; PlaylistManager({this.preloadPaddingCount = 3}) { _attachAudioSourceToPlayer(); _listenForCurrentSongIndexChanges(); } /// Used for loading the tracks. This will reset the current index and position. setQueue(List mediaItems) async { _fullPlaylist.clear(); _fullPlaylist.addAll(mediaItems); _subPlaylist.clear(); _subPlaylistItems.clear(); _lazyLoadPlaylist(); } /// Use this instead of [player.seekToNext] Future seekToNext() async { _directionOfSkipTo = 1; await _player.seekToNext(); } /// Use this instead of [player.seekToPrevious] Future seekToPrevious() async { _directionOfSkipTo = -1; await _player.seekToPrevious(); } Future _attachAudioSourceToPlayer() async { try { await _player.setAudioSource(_subPlaylist); } catch (e) { print("Error: $e"); } } void _listenForCurrentSongIndexChanges() { int? previousIndex = _player.currentIndex; _streamSubscriptions.add(_player.currentIndexStream.listen((index) { _updateMediaItem(); _lazyLoadPlaylist(); final previousIndex_ = previousIndex; previousIndex = index; if (previousIndex_ == null || index == null) return; final delta = index - previousIndex_; if (delta.sign == _directionOfSkipTo.sign) { _currentFullPlaylistIndex += delta; _playlistFrontLoadingCount += delta; } })); } int _getRelativeIndex(int offset) { return max(0, min(_currentFullPlaylistIndex + offset, _fullPlaylist.length - 1)); } _updateMediaItem() { if (_subPlaylistItems.isEmpty) return; final playerIndex = _player.currentIndex; if (playerIndex == null) return; final newMediaItem = _subPlaylistItems[playerIndex]; mediaItem.add(newMediaItem); } _lazyLoadPlaylist() async { // prevent race conditions if (_updatePlayerRunning) { _updatePlayerWasCalledWhileRunning = true; return; } _updatePlayerRunning = true; final currentIndex_ = _currentFullPlaylistIndex; final playerIndex = _player.currentIndex ?? 0; // Pad/pre-cache the ending of the playlist final currentNextPadding = max(0, _subPlaylist.length - playerIndex); var nextCountToAdd = preloadPaddingCount - currentNextPadding + 1; if (nextCountToAdd > 0 && _fullPlaylist.isNotEmpty) { for (final iNum in range(nextCountToAdd)) { var mediaItem = _fullPlaylist[iNum.toInt() + currentIndex_]; await _subPlaylist.add(_createAudioSource(mediaItem)); _subPlaylistItems.add(mediaItem); await Future.microtask(() {}); } } // Pad/pre-cache the beginning of the playlist final currentPreviousPadding = _player.currentIndex ?? 0; final previousCountToAdd = preloadPaddingCount - currentPreviousPadding; if (previousCountToAdd > 0) { for (int i = 1; i <= previousCountToAdd; i++) { var index = currentIndex_ - currentPreviousPadding - _playlistFrontLoadingCount - i; if (index < 0 || _fullPlaylist.length <= index) continue; var mediaItem = _fullPlaylist[index]; final future = _subPlaylist.insert(0, _createAudioSource(mediaItem)); _subPlaylistItems.insert(0, mediaItem); _playlistFrontLoadingCount++; await future; await Future.microtask(() {}); } } _updateMediaItem(); // prevent race conditions _updatePlayerRunning = false; if (_updatePlayerWasCalledWhileRunning) { _updatePlayerWasCalledWhileRunning = false; Future.microtask(() { _lazyLoadPlaylist(); }); } } UriAudioSource _createAudioSource(MediaItem mediaItem) { return AudioSource.uri( Uri.parse(mediaItem.extras!['url'] as String), tag: mediaItem, ); } Future dispose() async { for (final subscription in _streamSubscriptions) { await subscription.cancel(); } _streamSubscriptions.clear(); return _player.dispose(); } } class MyAudioHandler extends BaseAudioHandler { final _playlistManager = PlaylistManager(); final List _streamSubscriptions = []; MyAudioHandler() { _notifyAudioHandlerAboutPlaybackEvents(); _listenForDurationChanges(); _listenForCurrentSongIndexChanges(); } void _notifyAudioHandlerAboutPlaybackEvents() { _streamSubscriptions.add(_playlistManager.player.playbackEventStream.listen((PlaybackEvent event) { final playing = _playlistManager.player.playing; playbackState.add(playbackState.value.copyWith( controls: [ MediaControl.skipToPrevious, if (playing) MediaControl.pause else MediaControl.play, MediaControl.stop, MediaControl.skipToNext, ], systemActions: const { MediaAction.seek, }, androidCompactActionIndices: const [0, 1, 3], processingState: const { ProcessingState.idle: AudioProcessingState.idle, ProcessingState.loading: AudioProcessingState.loading, ProcessingState.buffering: AudioProcessingState.buffering, ProcessingState.ready: AudioProcessingState.ready, ProcessingState.completed: AudioProcessingState.completed, }[_playlistManager.player.processingState]!, repeatMode: const { LoopMode.off: AudioServiceRepeatMode.none, LoopMode.one: AudioServiceRepeatMode.one, LoopMode.all: AudioServiceRepeatMode.all, }[_playlistManager.player.loopMode]!, shuffleMode: (_playlistManager.player.shuffleModeEnabled) ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, playing: playing, updatePosition: _playlistManager.player.position, bufferedPosition: _playlistManager.player.bufferedPosition, speed: _playlistManager.player.speed, queueIndex: event.currentIndex, )); })); } void _listenForDurationChanges() { _streamSubscriptions.add(_playlistManager.player.durationStream.listen((duration) { final oldMediaItem = _playlistManager.mediaItem.valueOrNull; if (oldMediaItem == null) return; final newMediaItem = oldMediaItem.copyWith(duration: duration); mediaItem.add(newMediaItem); })); } void _listenForCurrentSongIndexChanges() { _streamSubscriptions.add(_playlistManager.mediaItem.listen((newMediaItem) { final newMediaItemWithDuration = newMediaItem.copyWith(duration: _playlistManager.player.duration); mediaItem.add(newMediaItemWithDuration); })); } @override Future addQueueItems(List mediaItems) async { throw UnimplementedError("addQueueItems"); } @override Future addQueueItem(MediaItem mediaItem) async { throw UnimplementedError("addQueueItem"); } @override Future updateQueue(List mediaItems) async { // notify system final newQueue = mediaItems; queue.add(newQueue); _playlistManager.setQueue(mediaItems); } @override Future removeQueueItemAt(int index) async { throw UnimplementedError("removeQueueItemAt"); } @override Future play() => _playlistManager.player.play(); @override Future pause() => _playlistManager.player.pause(); @override Future seek(Duration position) => _playlistManager.player.seek(position); @override Future skipToQueueItem(int index) async { throw UnimplementedError("skipToQueueItem"); } @override Future skipToNext() async { _playlistManager.seekToNext(); } @override Future skipToPrevious() async { if (_playlistManager.player.position.inSeconds > 5 || _playlistManager.currentIndex == 0) { return _playlistManager.player.seek(Duration.zero); } else { return _playlistManager.seekToPrevious(); } } @override Future setRepeatMode(AudioServiceRepeatMode repeatMode) async { throw UnimplementedError("setRepeatMode"); } @override Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { throw UnimplementedError("setShuffleMode"); } @override Future customAction(String name, [Map? extras]) async { if (name == 'dispose') { for (final subscription in _streamSubscriptions) { await subscription.cancel(); } _streamSubscriptions.clear(); await _playlistManager.dispose(); super.stop(); } } @override Future onTaskRemoved() async { stop(); super.onTaskRemoved(); } @override Future stop() async { await _playlistManager.player.stop(); return super.stop(); } } ```
hunterwilhelm commented 10 months ago

@ryanheise Is there a way to disable preloading/pre-caching entirely for ConcatenatingAudioSource? I don't see an option to turn it off on iOS.

ryanheise commented 10 months ago

No, that is in fact why you are working around it by only adding a limited number of items to the playlist. You need to have at least the current and the next item in the playlist in order to get gapless playback. If you want to get more sophisticated, you could listen to the current position, and when it reaches 15-20 seconds before reaching the end of the track, that's when you can add the next item to the playlist, so that it then starts buffering it just in time.

adamhaqiem commented 10 months ago

No, that is in fact why you are working around it by only adding a limited number of items to the playlist. You need to have at least the current and the next item in the playlist in order to get gapless playback. If you want to get more sophisticated, you could listen to the current position, and when it reaches 15-20 seconds before reaching the end of the track, that's when you can add the next item to the playlist, so that it then starts buffering it just in time.

Would it be a problem if I don't remove the previous item and just keep adding for as much as I want?

ryanheise commented 10 months ago

If you keep adding without ever removing, you will eventually end up with a very large list that could again slow down communication between Dart and iOS.

Eldar2021 commented 10 months ago

Can you please update branch(feature/treadmill)?

ryanheise commented 10 months ago

What specifically would you like me to update about it?

burhanaksendir commented 8 months ago

Dear @ryanheise, Firstly, congratulations on this fantastic package. I've encountered an issue – I've created an MP3 podcast playlist with no spaces at the beginning of the files, yet there's a disruptive effect during transitions. Is it possible to add a very brief gap at each episode change, or is there a way to smooth out the transitions?

ryanheise commented 8 months ago

Hi @burhanaksendir , this issue concerns slow loading of long playlists on iOS only. I would like to maintain separate issues for separate issues, so I'll mark both of our comments as off topic.

ulutashus commented 5 months ago

@ryanheise Is there any reason, known issue to not merge feature/treadmill branch? First impression, it works just fine for me. I am planning to publish it to our production if there is no known issue. Btw Is this branch changes affecting Android too?

ryanheise commented 5 months ago

I think one more thing I would like to do is make this behaviour sensitive to the useLazyPreparation parameter, or even add a separate parameter to specifically control the iOS/macOS specific behaviour. In this case, perhaps the parameter would control the size of the treadmill. I welcome any feedback on this.

ryanheise commented 4 months ago

or even add a separate parameter to specifically control the iOS/macOS specific behaviour. In this case, perhaps the parameter would control the size of the treadmill. I welcome any feedback on this.

The only reason to want to specify the size of the treadmill is for short queue items where items are so short that having two items on the treadmill is not enough of a buffer to ensure that we will not hit the end of the buffer and stall playback. However, I think a boolean option such as useLazyPreparation might suffice after all. Apps that are using "really" short items where this is a consideration probably aren't the intended use case for lazy loading anyway, and they have two options:

  1. Switch off the option. The entire playlist may load fast enough anyway if the items are really short.
  2. Implement their own treadmill in Dart using the mutating methods in ConcatenatingAudioSource.

So unless there are any objections, I may just tie this to useLazyPreparation with a treadmill size of 2.

ryanheise commented 4 months ago

Update: I have made substantial changes to the feature/treadmill branch in order to support the useLazyPreparation option, along with a couple of other bug fixes/enhancements.

Please let me know if this update breaks anything in your apps before I officially merge it into the next release.

MohammadElKhatib commented 4 months ago

Update: I have made substantial changes to the feature/treadmill branch in order to support the useLazyPreparation option, along with a couple of other bug fixes/enhancements.

Please let me know if this update breaks anything in your apps before I officially merge it into the next release.

Working great, I am using it to load 114 files with large files such as 2 hours length with no issues

bekchan commented 4 months ago

Update: I have made substantial changes to the feature/treadmill branch in order to support the useLazyPreparation option, along with a couple of other bug fixes/enhancements.

Please let me know if this update breaks anything in your apps before I officially merge it into the next release.

Works awesome with 1000+ ConcatenatingAudioSources in playlist.

ryanheise commented 4 months ago

Thanks for the testing results @MohammadElKhatib and @bekchan .

Have either of you had any experience with how this interacts with shuffle mode, loop modes, or edge cases such as where there are only 1 or 0 items in the playlist?

MohammadElKhatib commented 4 months ago

Thanks for the testing results @MohammadElKhatib and @bekchan .

Have either of you had any experience with how this interacts with shuffle mode, loop modes, or edge cases such as where there are only 1 or 0 items in the playlist?

Well I believe it is working so well as I mentioned previously, first it did not work well but maybe it was caching the default branch and once I cleared cache and re-run the application, it is working awesome

I am using it for Quran application where there is multiple reciters so basically 114 files with around 15 reciters and what i am doing is loading all the files and replace it once changing the reciters also I am listening to the sound progress updates in order to highlight text in parallel with sound.

thanks for your efforts waiting the merge with main branch :-)

ryanheise commented 4 months ago

Thanks, @MohammadElKhatib . Are you using either/both of shuffle and loop modes, or is that not a pertinent feature in your app?

MohammadElKhatib commented 4 months ago

Thanks, @MohammadElKhatib . Are you using either/both of shuffle and loop modes, or is that not a pertinent feature in your app?

I used the loop options but I faced one issue I am not sure if this is the default behavior, what happened is that I used a button to toggle the loop and repeat. I have 3 options repeat one, repeat all and no repeat basically repeat one is repeating the current sound normally, repeat all is going through all the files and repeat from first index after reaching the last index while the no repeat is keep going forward till the last sound and stop there.

while what I wanted is to only play one sound and stop so I am not sure if this is a bug or default behavior or I am missing something here.

Note: I am also using background service and syncing text and progress alltogether.

below screenshot of my app

IMG_8010

ryanheise commented 4 months ago

while what I wanted is to only play one sound and stop so I am not sure if this is a bug or default behavior or I am missing something here.

If you have a ConcatenatingAudioSource, it is the normal behaviour to auto-advance to the next child after completing the current one. There is another feature request to make that configurable, but until then you could listen to the PositionDiscontinuity event type and when it is autoAdvance, you can handle that event by pausing. The playlist example contains some code that shows how to listen for this event, since it uses it to show a toast whenever auto-advancing. Since this is a workaround until the aforementioned feature is implemented, it is not going to be perfect, though, because the time between when you receive the event and when you invoke pause() and wait for it to actually pause won't be zero, and so there is a chance that if the next audio doesn't have any silent padding at the start, you might hear a few milliseconds of the next audio before it pauses.

MohammadElKhatib commented 4 months ago

while what I wanted is to only play one sound and stop so I am not sure if this is a bug or default behavior or I am missing something here.

If you have a ConcatenatingAudioSource, it is the normal behaviour to auto-advance to the next child after completing the current one. There is another feature request to make that configurable, but until then you could listen to the PositionDiscontinuity event type and when it is autoAdvance, you can handle that event by pausing. The playlist example contains some code that shows how to listen for this event, since it uses it to show a toast whenever auto-advancing. Since this is a workaround until the aforementioned feature is implemented, it is not going to be perfect, though, because the time between when you receive the event and when you invoke pause() and wait for it to actually pause won't be zero, and so there is a chance that if the next audio doesn't have any silent padding at the start, you might hear a few milliseconds of the next audio before it pauses.

Honestly, the current situation is acceptable in my app, I will keep following your updates.

Thanks again for your great work.

bekchan commented 4 months ago

Have either of you had any experience with how this interacts with shuffle mode, loop modes, or edge cases such as where there are only 1 or 0 items in the playlist?

Shuffle and loop modes works great. Also tested reorderable playlist with +1000 audio, still good. Thank you for this great package.

ryanheise commented 4 months ago

I have just merged and published the feature/treadmill branch. Thank you very much to those who helped to test it. This issue will remain open since there are 2 other sources of lag that can be introduced by long lists. One is that the Dart iterators don't yield, which may be an issue on very long lists, and the other is (or was?) possibly the lag from passing a huge message over the method channels all at once. I have to test whether this is still an issue. But of course in the meantime, apps can work around this by creating an empty ConcatenatingAudioSource and incrementally building it up rather than initialising it all in one go.