diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 1de149bf9..fb3c19b2e 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -20,6 +20,9 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; /// * [x] Prefetch next track as [SpotubeTrack] on 80% of current track /// * [ ] Mixed Queue containing both [SpotubeTrack] and [LocalTrack] /// * [ ] Caching and loading of cache of tracks +/// * [ ] Shuffling and loop => playlist, track, none +/// * [ ] Alternative Track Source +/// * [x] Blacklisting of tracks and artist /// /// Don'ts: /// * It'll not have any proxy method for [SpotubeAudioPlayer] @@ -48,7 +51,8 @@ class ProxyPlaylistNotifier extends StateNotifier notificationService = await AudioServices.create(ref, this); audioPlayer.currentIndexChangedStream.listen((index) async { - if (index == -1) return; + if (index == -1 || index == state.active) return; + final track = state.tracks.elementAtOrNull(index); if (track == null) return; notificationService.addTrack(track); @@ -60,14 +64,26 @@ class ProxyPlaylistNotifier extends StateNotifier }); bool isPreSearching = false; - audioPlayer.percentCompletedStream(80).listen((_) async { + audioPlayer.percentCompletedStream(60).listen((percent) async { if (isPreSearching) return; try { isPreSearching = true; + final softReplace = + SpotubeAudioPlayer.mkSupportedPlatform && percent <= 98; + // TODO: Make repeat mode sensitive changes later - final track = - await ensureNthSourcePlayable(audioPlayer.currentIndex + 1); + final track = await ensureNthSourcePlayable( + audioPlayer.currentIndex + 1, + + /// [MediaKit] doesn't fully support replacing source, so we need + /// to check if the platform is supported or not and replace the + /// actual playlist with a playlist that contains the next track + /// at 98% >= progress + softReplace: softReplace, + exclusive: SpotubeAudioPlayer.mkSupportedPlatform, + ); + if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); } @@ -109,8 +125,7 @@ class ProxyPlaylistNotifier extends StateNotifier preferences.skipSponsorSegments) { for (final segment in activeTrack.skipSegments) { if (pos.inSeconds < segment["start"]! || - pos.inSeconds > segment["end"]!) continue; - await audioPlayer.pause(); + pos.inSeconds >= segment["end"]!) continue; await audioPlayer.seek(Duration(seconds: segment["end"]!)); } } @@ -118,7 +133,11 @@ class ProxyPlaylistNotifier extends StateNotifier }(); } - Future ensureNthSourcePlayable(int n) async { + Future ensureNthSourcePlayable( + int n, { + bool softReplace = false, + bool exclusive = false, + }) async { final sources = audioPlayer.sources; if (n < 0 || n > sources.length - 1) return null; final nthSource = sources.elementAtOrNull(n); @@ -127,19 +146,23 @@ class ProxyPlaylistNotifier extends StateNotifier final nthTrack = state.tracks.firstWhereOrNull( (element) => element.id == getIdFromUnPlayable(nthSource), ); - if (nthTrack == null || - nthTrack is SpotubeTrack || - nthTrack is LocalTrack) { + if (nthTrack == null || nthTrack is LocalTrack) { return null; } - final nthFetchedTrack = - await SpotubeTrack.fetchFromTrack(nthTrack, preferences); + final nthFetchedTrack = nthTrack is SpotubeTrack + ? nthTrack + : await SpotubeTrack.fetchFromTrack(nthTrack, preferences); - await audioPlayer.replaceSource( - nthSource, - nthFetchedTrack.ytUri, - ); + if (nthSource == nthFetchedTrack.ytUri) return null; + + if (!softReplace) { + await audioPlayer.replaceSource( + nthSource, + nthFetchedTrack.ytUri, + exclusive: exclusive, + ); + } return nthFetchedTrack; } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 54723c698..ef4250ece 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -64,15 +64,16 @@ class SpotubeAudioPlayer { } /// Stream that emits when the player is almost (%) complete - Stream percentCompletedStream(double percent) { + Stream percentCompletedStream(double percent) { return positionStream - .asyncMap((event) async => [event, await duration]) - .where((event) { - final position = event[0] as Duration; - final duration = event[1] as Duration; - - return position.inSeconds > duration.inSeconds * percent / 100; - }).asBroadcastStream(); + .asyncMap( + (position) async => (await duration)?.inSeconds == 0 + ? 0 + : (position.inSeconds / (await duration)!.inSeconds * 100) + .toInt(), + ) + .where((event) => event >= percent) + .asBroadcastStream(); } Stream get playingStream { @@ -143,12 +144,8 @@ class SpotubeAudioPlayer { .map((event) => event.index) .asBroadcastStream(); } else { - return _justAudio!.positionDiscontinuityStream - .where( - (event) => - event.reason == ja.PositionDiscontinuityReason.autoAdvance, - ) - .map((event) => currentIndex) + return _justAudio!.sequenceStateStream + .map((event) => event?.currentIndex ?? -1) .asBroadcastStream(); } } @@ -237,6 +234,7 @@ class SpotubeAudioPlayer { } } + /// Returns the current volume of the player, between 0 and 1 double get volume { if (mkSupportedPlatform) { return _mkPlayer!.state.volume / 100; @@ -320,8 +318,10 @@ class SpotubeAudioPlayer { await _justAudio?.seek(position); } + /// Volume is between 0 and 1 Future setVolume(double volume) async { - await _mkPlayer?.setVolume(volume); + assert(volume >= 0 && volume <= 1); + await _mkPlayer?.setVolume(volume * 100); await _justAudio?.setVolume(volume); } @@ -391,7 +391,7 @@ class SpotubeAudioPlayer { if (mkSupportedPlatform) { return _mkPlayer!.state.playlist.index; } else { - return _justAudio!.sequenceState!.currentIndex; + return _justAudio!.sequenceState?.currentIndex ?? -1; } } @@ -447,13 +447,18 @@ class SpotubeAudioPlayer { } } - Future replaceSource(String oldSource, String newSource) async { - final willBeReplacedIndex = sources.indexOf(oldSource); - if (willBeReplacedIndex == -1) return; + Future replaceSource( + String oldSource, + String newSource, { + bool exclusive = false, + }) async { + final oldSourceIndex = sources.indexOf(oldSource); + if (oldSourceIndex == -1) return; if (mkSupportedPlatform) { final sourcesCp = sources.toList(); - sourcesCp[willBeReplacedIndex] = newSource; + sourcesCp[oldSourceIndex] = newSource; + await _mkPlayer!.open( mk.Playlist( sourcesCp.map(mk.Media.new).toList(), @@ -461,20 +466,21 @@ class SpotubeAudioPlayer { ), play: false, ); + if (exclusive) await jumpTo(oldSourceIndex); } else { await addTrack(newSource); - await removeTrack(willBeReplacedIndex); + await removeTrack(oldSourceIndex); int newSourceIndex = sources.indexOf(newSource); while (newSourceIndex == -1) { await Future.delayed(const Duration(milliseconds: 100)); newSourceIndex = sources.indexOf(newSource); } - await moveTrack(newSourceIndex, willBeReplacedIndex); + await moveTrack(newSourceIndex, oldSourceIndex); newSourceIndex = sources.indexOf(newSource); - while (newSourceIndex != willBeReplacedIndex) { + while (newSourceIndex != oldSourceIndex) { await Future.delayed(const Duration(milliseconds: 100)); - await moveTrack(newSourceIndex, willBeReplacedIndex); + await moveTrack(newSourceIndex, oldSourceIndex); newSourceIndex = sources.indexOf(newSource); } }