Open arshrahman opened 3 years ago
This is an important feature. I'm not sure on the best API, so I'd like to think about alternative API designs before choosing one.
@ryanheise thanks for the swift response. Are there any workaround approaches that I can implement now?
The alternatives would be to not use a ConcatenatingAudioSource
. You could just define your own list of items:
final List<AudioSource> items = ...;
And to play one, do something like this:
Future<void> playItem(int index) async {
await _player.setAudioSource(items[index]);
await _player.play();
await _player.pause();
}
This may or may not be efficient in terms of audio buffer loading, depending on your requirements, but you can test it and see. If it's not efficient enough and you want to preload all items, you may need to create items.length
different players, each one loading just one item and holding it for when you need it. But this would use a lot more resources so you'd also need to test whether you're running into OS limits, depending on how many items you're simultaneously loading into different players.
I managed to do a workaround based on your suggestion with some tweaks. I repurposed LoopMode.off
to behave like LoopMode.once
.
So, when setting AudioSource
, I check LoopMode
.
int nowPlayingIndex;
Future<void> setAudioSourceWithInitialPosition({int initialIndex, Duration initialPosition = Duration.zero}) async {
nowPlayingIndex = initialIndex;
if (audioPlayer.loopMode == LoopMode.off)
audioPlayer.setAudioSource(queue[initialIndex], initialPosition: initialPosition);
else
audioPlayer.setAudioSource(
ConcatenatingAudioSource(children: queue),
preload: false,
initialIndex: initialIndex,
initialPosition: initialPosition,
);
}
Then, I keep track of the nowPlayingIndex
so that when LoopMode
changes, there will be a smooth transition.
audioPlayer.durationStream.listen((duration) {
if (audioPlayer.loopMode == LoopMode.off) return;
final index = audioPlayer.currentIndex;
if (index != null && queue != null && queue.length > index) {
queue[index] = queue[index].copyWith(duration: duration);
nowPlayingIndex = index;
}));
}
});
I reset AudioSource
again when LoopMode
changes.
await audioPlayer.setLoopMode(loopMode);
if (nowPlayingIndex != null)
setAudioSourceWithInitialPosition(initialIndex: nowPlayingDuaIndex, initialPosition: audioPlayer.position);
Lastly, I disabled skipToPrevious
and skipToNext
when LoopMode
is off
This works though I need to do more testing and this really is a workaround.
forgot to include this,
audioPlayer.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
if (audioPlayer.loopMode == LoopMode.off) onPause();
break;
default:
break;
}
});
this handles the playback state and calls pause
when playing
is completed
If your only working with LoopMode.all this should be an easy way to achive your goal:
_handleAndroidAutoPause(){ Rx.combineLatest2<Duration, Duration, PositionData>(androidPlayer.positionStream, androidPlayer.durationStream, (position, duration) => PositionData(position: position, duration: duration)).listen((positionData) { if(positionData.position.inSeconds>=positionData.duration.inSeconds && androidPlayer.playing) { androidPlayer.seekToNext().then((value) => androidPlayer.pause()); } }); }
just call that method after player init
@xxjonesriderxx unfortunately you can't rely on pause
to happen quickly enough to definitely prevent a bit of sound coming out from the next playlist item. Maybe that is sufficient for some use cases though, but if not, the workaround would be to take the ConcatenatingAudioSource
into your own hands and position the current item as the last item temporarily using the move
method.
just adding my two cents into the convo, this feature would be a lifesaver for me, I've implemented this in a very fragile way but it seems to work for now (and as long as it's stable enough for me to gather data at the moment, then it's good enough, but not for future plans.
I've done it as @ryanheise suggested using ConcatenatingAudioSource
and a listener for playbackEventStream
Here's a generic version of what I've done:
enum PlaybackState {
playing,
completed,
stopped,
}
class MyPlayerWrapper {
final player = AudioPlayer(handleInterruptions: false);
static ConcatenatingAudioSource? _experimentPlaylist;
bool _playlistLoaded = false;
VoidCallback? _onPlayComplete;
int _currentIndex = -1;
MyPlayerWrapper({VoidCallback? onPlayComplete}) {
if (onPlayComplete != null) {
this.onPlayComplete = onPlayComplete;
}
}
Future<void> loadExperiment() async {
// prepare playlist here
if (!_playlistLoaded) {
await player.setAudioSource(_experimentPlaylist!);
_playlistLoaded = true;
player.playbackEventStream.listen((event) async {
if (_playerState == PlaybackState.playing &&
event.processingState == ProcessingState.completed) {
if (event.updatePosition >= event.duration!) {
stateChangeListener(PlaybackState.completed);
Future.delayed(Duration(milliseconds: 100), () async {
debugPrint("Pausing playback...");
await player.pause();
debugPrint("Moving item ${event.currentIndex} to $_currentIndex");
await _experimentPlaylist!
.move(event.currentIndex!, _currentIndex);
});
}
}
});
}
// handle state manually
PlaybackState _playerState = PlaybackState.stopped;
Future<void> play() {
int lastIndex = _experimentPlaylist!.length - 1;
await _experimentPlaylist!.move(_currentIndex, lastIndex).then((_) async {
debugPrint(
"moved target (index: $_currentIndex) to end of playlist (index: $lastIndex)");
if (player.currentIndex != lastIndex) {
await player.seek(Duration.zero, index: lastIndex);
debugPrint("Player seek to $lastIndex");
}
await player.play();
});
}
void stateChangeListener(PlaybackState state) {
debugPrint('StateChangeListener: $state');
if (_playerState == state) {
debugPrint('no state change, returning');
return;
}
_playerState = state;
if (state == PlaybackState.completed) {
_onPlayComplete?.call();
}
}
// for externally setting index/step
set currentIndex(int index) {
_currentIndex = index;
}
}
Obviously the above is untested as I ripped out all the project-specific code, but it works so far using this method.
I do have one question about this implementation though, why does it fail with a Bad state: Cannot fire new event. Controller is already firing an event
exception when I don't include the Future.delayed
in the playbackEventStream.listen
callback?
I forgot to say huge thanks to @ryanheise for all the hard work! It's definitely appreciated and the fact that I could even workaround like this means I can have consistent timing for my experiment trial pages, which is absolutely critical (I tried every way with audioplayers
and nothing could stop some audio clips, even repeated ones, taking ages to load intermittently)...so thank you!
Edit: while I'm happy I switched to just_audio
I realised I may have thrown unnecessary shade at audioplayers
(at least not confirmed) because I found a bug (?) in the flutter framework causing async events to not fire for really long periods of time (5+ seconds) when I had a placeholder StatelessWidget
waiting on a ChangeNotifierProvider
, to force event firing I had to put a dirty while
loop with a notifyListeners
every 25ms to force event firing (and unfortunately some widget rebuilding)...
audioPrompt.onPlayStart = () async {
while (!promptComplete) {
await Future.delayed(const Duration(milliseconds: 25), () {
notifyListeners();
});
}
};
Hi @zeyus , thanks for sharing your implementation. It should come in handy to anyone else who is trying to solve the same use case while waiting for a feature explicitly for this.
Regarding the error, it might be related to (or the same as) this:
https://stackoverflow.com/questions/66513675/silence-between-tracks-in-just-audio/66514017#66514017
@ryanheise that's great, thanks just checked it out and it makes sense to me now! :)
Is your feature request related to a problem? Please describe.
ConcatenatingAudioSource
plays all mediaItem in it. There's no option to play a particularMediaItem
only once and then pause theAudioplayer
once the mediaItem completes playing.Listening to
ProcessingState
also doesn't seem to solve this problem sinceProcessingState.completed
is only called when the lastMediaItem
completes playing.Describe the solution you'd like A solution would be add a new looping option
LoopMode.once
which plays the mediaItem exactly once and then pauses theAudioplayer
.Describe alternatives you've considered I have tried the following code snippet but it doesn't work.
If there's any other way to achieve this, that would be great.
Additional context None