From 3ba3df726597b384906cc73ea90ea8af1ff38dd1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 12 May 2023 20:35:48 +0600 Subject: [PATCH] refactor(playlist_queue): add playlist 3 items load first support --- lib/collections/intents.dart | 3 +- lib/components/player/player_controls.dart | 25 +- lib/pages/album/album.dart | 3 +- lib/provider/playlist_queue_provider.dart | 320 +++++++++--------- lib/services/audio_player/audio_player.dart | 38 ++- lib/services/audio_player/loop_mode.dart | 61 ++++ .../audio_services/linux_audio_service.dart | 17 +- .../audio_services/mobile_audio_service.dart | 22 +- 8 files changed, 280 insertions(+), 209 deletions(-) diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 398bf4a4..2ad5bdd7 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -31,7 +31,8 @@ class PlayPauseAction extends Action { if (audioPlayer.hasSource && !await audioPlayer.isCompleted) { await playlistNotifier.resume(); } else { - await playlistNotifier.play(); + // TODO: Implement play on start + // await playlistNotifier.play(); } } else { await playlistNotifier.pause(); diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index 145af90c..fe13aaa4 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -11,6 +11,7 @@ import 'package:spotube/hooks/use_progress.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/utils/primitive_utils.dart'; class PlayerControls extends HookConsumerWidget { @@ -186,20 +187,20 @@ class PlayerControls extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( - tooltip: playlist?.isShuffled == true + tooltip: playlist?.shuffled == true ? context.l10n.unshuffle_playlist : context.l10n.shuffle_playlist, icon: const Icon(SpotubeIcons.shuffle), - style: playlist?.isShuffled == true + style: playlist?.shuffled == true ? activeButtonStyle : buttonStyle, onPressed: playlist == null || playlist.isLoading ? null : () { - if (playlist.isShuffled == true) { - playlistNotifier.unshuffle(); + if (playlist.shuffled == true) { + playlistNotifier.setShuffle(false); } else { - playlistNotifier.shuffle(); + playlistNotifier.setShuffle(true); } }, ), @@ -240,24 +241,26 @@ class PlayerControls extends HookConsumerWidget { onPressed: playlistNotifier.next, ), IconButton( - tooltip: playlist?.isLooping != true + tooltip: playlist?.loopMode == PlaybackLoopMode.one ? context.l10n.loop_track : context.l10n.repeat_playlist, icon: Icon( - playlist?.isLooping == true + playlist?.loopMode == PlaybackLoopMode.one ? SpotubeIcons.repeatOne : SpotubeIcons.repeat, ), - style: playlist?.isLooping == true + style: playlist?.loopMode == PlaybackLoopMode.one ? activeButtonStyle : buttonStyle, onPressed: playlist == null || playlist.isLoading ? null : () { - if (playlist.isLooping == true) { - playlistNotifier.unloop(); + if (playlist.loopMode == PlaybackLoopMode.one) { + playlistNotifier + .setLoopMode(PlaybackLoopMode.all); } else { - playlistNotifier.loop(); + playlistNotifier + .setLoopMode(PlaybackLoopMode.one); } }, ), diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index abf86f3f..2acd783a 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -28,11 +28,10 @@ class AlbumPage extends HookConsumerWidget { currentTrack ??= sortedTracks.first; final isPlaylistPlaying = playback.isPlayingPlaylist(tracks); if (!isPlaylistPlaying) { - playback.load( + await playback.loadAndPlay( sortedTracks, active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), ); - await playback.play(); } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != playlist?.activeTrack.id) { diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index 1a9e4b8a..baf26294 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -12,6 +12,7 @@ import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -20,16 +21,18 @@ import 'package:collection/collection.dart'; class PlaylistQueue { final Set tracks; - final Set tempTracks; - final bool loop; final int active; Track get activeTrack => tracks.elementAt(active); + final bool shuffled; + final PlaybackLoopMode loopMode; + static Future fromJson( - Map json, UserPreferences preferences) async { + Map json, + UserPreferences preferences, + ) async { final List? tracks = json['tracks']; - final List? tempTracks = json['tempTracks']; return PlaylistQueue( Set.from( await Future.wait( @@ -54,28 +57,8 @@ class PlaylistQueue { ), ), active: json['active'], - tempTracks: Set.from( - await Future.wait( - tempTracks?.mapIndexed( - (i, e) async { - final jsonTrack = - Map.castFrom(e); - - if (e["path"] != null) { - return LocalTrack.fromJson(jsonTrack); - } else if (i == json["active"] && !json.containsKey("path")) { - return await SpotubeTrack.fetchFromTrack( - Track.fromJson(jsonTrack), - preferences, - ); - } else { - return Track.fromJson(jsonTrack); - } - }, - ) ?? - [], - ), - ), + shuffled: json['shuffled'], + loopMode: PlaybackLoopMode.fromString(json['loopMode'] ?? ''), ); } @@ -93,43 +76,32 @@ class PlaylistQueue { }, ).toList(), 'active': active, - 'tempTracks': tempTracks.map( - (e) { - if (e is SpotubeTrack) { - return e.toJson(); - } else if (e is LocalTrack) { - return e.toJson(); - } else { - return e.toJson(); - } - }, - ).toList(), + 'shuffled': shuffled, + 'loopMode': loopMode.name, }; } bool get isLoading => activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack; - bool get isShuffled => tempTracks.isNotEmpty; - bool get isLooping => loop; PlaylistQueue( this.tracks, { - required this.tempTracks, this.active = 0, - this.loop = false, + this.shuffled = false, + this.loopMode = PlaybackLoopMode.none, }) : assert(active < tracks.length && active >= 0, "Invalid active index"); PlaylistQueue copyWith({ Set? tracks, - Set? tempTracks, int? active, - bool? loop, + bool? shuffled, + PlaybackLoopMode? loopMode, }) { return PlaylistQueue( tracks ?? this.tracks, active: active ?? this.active, - tempTracks: tempTracks ?? this.tempTracks, - loop: loop ?? this.loop, + shuffled: shuffled ?? this.shuffled, + loopMode: loopMode ?? this.loopMode, ); } } @@ -153,13 +125,67 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { void configure() async { audioServices = await AudioServices.create(ref, this); - audioPlayer.completedStream.listen((event) async { + audioPlayer.currentIndexChangedStream.listen((index) async { if (!isLoaded) return; - if (state!.isLooping) { - await audioPlayer.seek(Duration.zero); - await audioPlayer.resume(); - } else { - await next(); + state = state!.copyWith(active: index); + await audioServices.addTrack(state!.activeTrack); + }); + + audioPlayer.almostCompleteStream.listen((_) async { + if (!isLoaded) return; + final nextTrack = state!.tracks.elementAtOrNull(state!.active + 1); + final sources = audioPlayer.sources; + + // we don't have a next track or the next track is already loaded + // only when the next track isn't loaded we load next 3 tracks + if (nextTrack == null || + nextTrack is SpotubeTrack && sources.contains(nextTrack.ytUri)) { + return; + } + + final List fetchedTracks = []; + + // load next 3 tracks + final tracks = await Future.wait(state!.tracks + .toList() + .skip(state!.active + 1) + .take(3) + .mapIndexed((i, track) async { + if (track is LocalTrack) return Future.value(track.path); + if (track is SpotubeTrack) return Future.value(track.ytUri); + if (i == 0) { + final fetchedTrack = + await SpotubeTrack.fetchFromTrack(track, preferences); + fetchedTracks.add(fetchedTrack); + return fetchedTrack.ytUri; + } + // Adding delay to not spoof the YouTube API for IP Block + final fetchedTrack = await Future.delayed( + const Duration(milliseconds: 100), + () => SpotubeTrack.fetchFromTrack(track, preferences), + ); + + fetchedTracks.add(fetchedTrack); + return fetchedTrack.ytUri; + })); + + // replacing the tracks with the fetched tracks + // in proxy playlist + state = state!.copyWith( + tracks: state!.tracks.map((track) { + final fetchedTrack = + fetchedTracks.firstWhereOrNull((e) => e.id == track.id); + + if (fetchedTrack != null) { + return fetchedTrack; + } + return track; + }).toSet(), + ); + + for (final track in tracks) { + if (sources.contains(track)) continue; + await audioPlayer.addTrack(track); } }); @@ -241,6 +267,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } } + // TODO: Removal of track support void remove(List tracks) { if (!isLoaded) return; final trackIds = tracks.map((e) => e.id!).toSet(); @@ -260,50 +287,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { : state!.active : null, ); - if (state!.isLoading) { - play(); - } - } - - void shuffle() { - if (!isLoaded || state!.isShuffled) return; - state = state?.copyWith( - tempTracks: state!.tracks, - tracks: { - state!.activeTrack, - ...state!.tracks.toList() - ..removeAt(state!.active) - ..shuffle() - }, - active: 0, - ); - } - - void unshuffle() { - if (!isLoaded || !state!.isShuffled) return; - state = state?.copyWith( - tracks: state!.tempTracks, - active: state!.tempTracks - .toList() - .indexWhere((element) => element.id == state!.activeTrack.id), - tempTracks: {}, - ); - } - - void loop() { - if (!isLoaded || state!.isLooping) return; - state = state?.copyWith( - loop: true, - ); - } - - void unloop() { - if (!isLoaded || !state!.isLooping) return; - state = state?.copyWith( - loop: false, - ); + // if (state!.isLoading) { + // play(); + // } } + // TODO: Swap sibling support Future swapSibling(Video video) async { if (!isLoaded || state!.isLoading) return; await pause(); @@ -314,7 +303,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { tracks[state!.active] = track; state = state!.copyWith(tracks: Set.from(tracks)); - await play(); + // await play(); } Future populateSibling() async { @@ -325,66 +314,92 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { state = state!.copyWith(tracks: Set.from(tracks)); } - Future play() async { - if (!isLoaded) return; - await pause(); - await audioServices.addTrack(state!.activeTrack); - if (state!.activeTrack is LocalTrack) { - await audioPlayer.play((state!.activeTrack as LocalTrack).path); - return; - } - if (state!.activeTrack is! SpotubeTrack) { - final tracks = state!.tracks.toList(); - tracks[state!.active] = await SpotubeTrack.fetchFromTrack( - state!.activeTrack, - preferences, - ); - final tempTracks = state!.tempTracks - .map((e) => - e.id == tracks[state!.active].id ? tracks[state!.active] : e) - .toList(); + // Future play() async { + // if (!isLoaded) return; + // await pause(); + // await audioServices.addTrack(state!.activeTrack); + // if (state!.activeTrack is LocalTrack) { + // await audioPlayer.play((state!.activeTrack as LocalTrack).path); + // return; + // } + // if (state!.activeTrack is! SpotubeTrack) { + // final tracks = state!.tracks.toList(); + // tracks[state!.active] = await SpotubeTrack.fetchFromTrack( + // state!.activeTrack, + // preferences, + // ); - state = state!.copyWith( - tracks: Set.from(tracks), - tempTracks: Set.from(tempTracks), - ); - } + // state = state!.copyWith(tracks: Set.from(tracks)); + // } - await audioServices.addTrack(state!.activeTrack); + // await audioServices.addTrack(state!.activeTrack); - final cached = - await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!); - if (preferences.predownload && cached != null) { - await audioPlayer.play(cached.file.path); - } else { - await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri); - } - } + // final cached = + // await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!); + // if (preferences.predownload && cached != null) { + // await audioPlayer.play(cached.file.path); + // } else { + // await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri); + // } + // } + // TODO: Implement Playtrack Future playTrack(Track track) async { if (!isLoaded) return; final active = state!.tracks.toList().indexWhere((element) => element.id == track.id); if (active == -1) return; state = state!.copyWith(active: active); - return play(); } - void load(Iterable tracks, {int active = 0}) { + Future load(Iterable tracks, {int active = 0}) async { final activeTrack = tracks.elementAt(active); final filtered = Set.from(blacklist.filter(tracks)); state = PlaylistQueue( Set.from(blacklist.filter(tracks)), - tempTracks: {}, active: filtered .toList() .indexWhere((element) => element.id == activeTrack.id), ); + + // load 3 items first to avoid huge initial loading time + final firstTracks = await Future.wait( + filtered + .skip(active == 0 ? 0 : active - 1) + .take(3) + .mapIndexed((i, track) { + if (track is LocalTrack) return Future.value(track.path); + if (i == 0) { + return SpotubeTrack.fetchFromTrack(track, preferences).then( + (s) => s.ytUri, + ); + } + // Adding delay to not spoof the YouTube API for IP Block + return Future.delayed( + const Duration(milliseconds: 100), + () => SpotubeTrack.fetchFromTrack(track, preferences).then( + (s) => s.ytUri, + ), + ); + }), + ); + + final localTracks = tracks + .where( + (element) => + element is LocalTrack && !firstTracks.contains(element.path), + ) + .map((e) => (e as LocalTrack).path); + + await audioPlayer.openPlaylist( + [...firstTracks, ...localTracks], + autoPlay: false, + ); } Future loadAndPlay(Iterable tracks, {int active = 0}) async { - load(tracks, active: active); - await play(); + await load(tracks, active: active); + await resume(); } Future pause() { @@ -403,31 +418,11 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } Future next() async { - if (!isLoaded) return; - if (state!.active == state!.tracks.length - 1) { - state = state!.copyWith( - active: 0, - ); - } else { - state = state!.copyWith( - active: state!.active + 1, - ); - } - return play(); + return audioPlayer.skipToNext(); } Future previous() async { - if (!isLoaded) return; - if (state!.active == 0) { - state = state!.copyWith( - active: state!.tracks.length - 1, - ); - } else { - state = state!.copyWith( - active: state!.active - 1, - ); - } - return play(); + return audioPlayer.skipToPrevious(); } Future seek(Duration position) async { @@ -436,12 +431,23 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { await resume(); } + Future setShuffle(bool shuffle) async { + if (!isLoaded) return; + audioPlayer.setShuffle(shuffle); + state = state!.copyWith(shuffled: await audioPlayer.isShuffled()); + } + + Future setLoopMode(PlaybackLoopMode loopMode) async { + if (!isLoaded) return; + audioPlayer.setLoopMode(loopMode); + state = state!.copyWith(loopMode: loopMode); + } + // utility bool isPlayingPlaylist(Iterable playlist) { if (!isLoaded || playlist.isEmpty) return false; - final trackIds = (state!.isShuffled ? state!.tempTracks : state!.tracks) - .map((track) => track.id!); + final trackIds = state!.tracks.map((track) => track.id!); return blacklist .filter(playlist) .every((track) => trackIds.contains(track.id!)); @@ -449,10 +455,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { bool isTrackOnQueue(TrackSimple track) { if (!isLoaded) return false; - if (state!.isShuffled) { - final trackIds = state!.tempTracks.map((track) => track.id!); - return trackIds.contains(track.id!); - } final trackIds = state!.tracks.map((track) => track.id!); return trackIds.contains(track.id!); } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 511e4b5e..412047d3 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -107,6 +107,22 @@ class SpotubeAudioPlayer { } } + Stream get currentIndexChangedStream { + if (mkSupportedPlatform) { + return _mkPlayer!.streams.playlist + .map((event) => event.index) + .asBroadcastStream(); + } else { + return _justAudio!.positionDiscontinuityStream + .where( + (event) => + event.reason == ja.PositionDiscontinuityReason.autoAdvance, + ) + .map((event) => currentIndex) + .asBroadcastStream(); + } + } + // regular info getter Future get duration async { @@ -301,22 +317,24 @@ class SpotubeAudioPlayer { } List resolveTracksForSource(List tracks) { - if (mkSupportedPlatform) { - final urls = _mkPlayer!.state.playlist.medias.map((e) => e.uri).toList(); - return tracks.where((e) => urls.contains(e.ytUri)).toList(); - } else { - final urls = (_justAudio!.audioSource as ja.ConcatenatingAudioSource) - .children - .map((e) => (e as ja.UriAudioSource).uri.toString()) - .toList(); - return tracks.where((e) => urls.contains(e.ytUri)).toList(); - } + return tracks.where((e) => sources.contains(e.ytUri)).toList(); } bool tracksExistsInPlaylist(List tracks) { return resolveTracksForSource(tracks).length == tracks.length; } + List get sources { + if (mkSupportedPlatform) { + return _mkPlayer!.state.playlist.medias.map((e) => e.uri).toList(); + } else { + return (_justAudio!.audioSource as ja.ConcatenatingAudioSource) + .children + .map((e) => (e as ja.UriAudioSource).uri.toString()) + .toList(); + } + } + int get currentIndex { if (mkSupportedPlatform) { return _mkPlayer!.state.playlist.index; diff --git a/lib/services/audio_player/loop_mode.dart b/lib/services/audio_player/loop_mode.dart index 7f767c44..a55b2298 100644 --- a/lib/services/audio_player/loop_mode.dart +++ b/lib/services/audio_player/loop_mode.dart @@ -1,5 +1,7 @@ +import 'package:audio_service/audio_service.dart'; import 'package:media_kit/media_kit.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:mpris_service/mpris_service.dart'; /// An unified loop mode for both [LoopMode] and [PlaylistMode] enum PlaybackLoopMode { @@ -50,4 +52,63 @@ enum PlaybackLoopMode { return PlaylistMode.none; } } + + static PlaybackLoopMode fromMPRISLoopStatus(MPRISLoopStatus status) { + switch (status) { + case MPRISLoopStatus.none: + return PlaybackLoopMode.none; + case MPRISLoopStatus.track: + return PlaybackLoopMode.one; + case MPRISLoopStatus.playlist: + return PlaybackLoopMode.all; + } + } + + MPRISLoopStatus toMPRISLoopStatus() { + switch (this) { + case PlaybackLoopMode.all: + return MPRISLoopStatus.playlist; + case PlaybackLoopMode.one: + return MPRISLoopStatus.track; + case PlaybackLoopMode.none: + return MPRISLoopStatus.none; + } + } + + static PlaybackLoopMode fromAudioServiceRepeatMode( + AudioServiceRepeatMode mode) { + switch (mode) { + case AudioServiceRepeatMode.all: + case AudioServiceRepeatMode.group: + return PlaybackLoopMode.all; + case AudioServiceRepeatMode.one: + return PlaybackLoopMode.one; + case AudioServiceRepeatMode.none: + return PlaybackLoopMode.none; + } + } + + AudioServiceRepeatMode toAudioServiceRepeatMode() { + switch (this) { + case PlaybackLoopMode.all: + return AudioServiceRepeatMode.all; + case PlaybackLoopMode.one: + return AudioServiceRepeatMode.one; + case PlaybackLoopMode.none: + return AudioServiceRepeatMode.none; + } + } + + static PlaybackLoopMode fromString(String? value) { + switch (value) { + case 'all': + return PlaybackLoopMode.all; + case 'one': + return PlaybackLoopMode.one; + case 'none': + return PlaybackLoopMode.none; + default: + return PlaybackLoopMode.none; + } + } } diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart index c8d986fa..840695ff 100644 --- a/lib/services/audio_services/linux_audio_service.dart +++ b/lib/services/audio_services/linux_audio_service.dart @@ -7,6 +7,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -29,11 +30,9 @@ class LinuxAudioService { mpris.playbackStatus = MPRISPlaybackStatus.stopped; mpris.setEventHandler(MPRISEventHandler( loopStatus: (value) async { - if (value == MPRISLoopStatus.none) { - playlistNotifier.unloop(); - } else if (value == MPRISLoopStatus.track) { - playlistNotifier.loop(); - } + playlistNotifier.setLoopMode( + PlaybackLoopMode.fromMPRISLoopStatus(value), + ); }, next: playlistNotifier.next, pause: playlistNotifier.pause, @@ -46,13 +45,7 @@ class LinuxAudioService { } }, seek: playlistNotifier.seek, - shuffle: (value) async { - if (value) { - playlistNotifier.shuffle(); - } else { - playlistNotifier.unshuffle(); - } - }, + shuffle: playlistNotifier.setShuffle, stop: playlistNotifier.stop, volume: (value) async { await ref.read(VolumeProvider.provider.notifier).setVolume(value); diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 233fa3d0..97ccd3ad 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -4,6 +4,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/loop_mode.dart'; class MobileAudioService extends BaseAudioHandler { AudioSession? session; @@ -58,21 +59,15 @@ class MobileAudioService extends BaseAudioHandler { Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { await super.setShuffleMode(shuffleMode); - if (shuffleMode == AudioServiceShuffleMode.all) { - playlistNotifier.shuffle(); - } else { - playlistNotifier.unshuffle(); - } + playlistNotifier.setShuffle(shuffleMode == AudioServiceShuffleMode.all); } @override Future setRepeatMode(AudioServiceRepeatMode repeatMode) async { super.setRepeatMode(repeatMode); - if (repeatMode == AudioServiceRepeatMode.all) { - playlistNotifier.loop(); - } else { - playlistNotifier.unloop(); - } + playlistNotifier.setLoopMode( + PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode), + ); } @override @@ -114,12 +109,11 @@ class MobileAudioService extends BaseAudioHandler { playing: audioPlayer.isPlaying, updatePosition: position, bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero, - shuffleMode: playlist?.isShuffled == true + shuffleMode: playlist?.shuffled == true ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, - repeatMode: playlist?.isLooping == true - ? AudioServiceRepeatMode.one - : AudioServiceRepeatMode.all, + repeatMode: playlist?.loopMode.toAudioServiceRepeatMode() ?? + AudioServiceRepeatMode.none, processingState: playlist?.isLoading == true ? AudioProcessingState.loading : AudioProcessingState.ready,