diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 6eefcae7..2a2a86f3 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -74,20 +74,9 @@ class ProxyPlaylistNotifier extends StateNotifier try { isPreSearching = true; - final softReplace = - SpotubeAudioPlayer.mkSupportedPlatform && percent <= 98; - // TODO: Make repeat mode sensitive changes later - 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, - ); + final track = + await ensureNthSourcePlayable(audioPlayer.currentIndex + 1); if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); @@ -190,17 +179,17 @@ class ProxyPlaylistNotifier extends StateNotifier // TODO: Safely Remove playing tracks - void removeTrack(String trackId) { + Future removeTrack(String trackId) async { final track = state.tracks.firstWhereOrNull((element) => element.id == trackId); if (track == null) return; state = state.copyWith(tracks: {...state.tracks..remove(track)}); final index = audioPlayer.sources.indexOf(makeAppropriateSource(track)); if (index == -1) return; - audioPlayer.removeTrack(index); + await audioPlayer.removeTrack(index); } - void removeTracks(Iterable tracksIds) { + Future removeTracks(Iterable tracksIds) async { final tracks = state.tracks.where((element) => tracksIds.contains(element.id)); @@ -211,7 +200,7 @@ class ProxyPlaylistNotifier extends StateNotifier for (final track in tracks) { final index = audioPlayer.sources.indexOf(makeAppropriateSource(track)); if (index == -1) continue; - audioPlayer.removeTrack(index); + await audioPlayer.removeTrack(index); } } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index ef4250ec..c5cb7529 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:catcher/catcher.dart'; import 'package:collection/collection.dart'; import 'package:media_kit/media_kit.dart' as mk; import 'package:just_audio/just_audio.dart' as ja; @@ -17,7 +18,11 @@ class SpotubeAudioPlayer { SpotubeAudioPlayer() : _mkPlayer = mkSupportedPlatform ? MkPlayerWithState() : null, - _justAudio = !mkSupportedPlatform ? ja.AudioPlayer() : null; + _justAudio = !mkSupportedPlatform ? ja.AudioPlayer() : null { + _mkPlayer?.streams.error.listen((event) { + Catcher.reportCheckedError(event, StackTrace.current); + }); + } /// Whether the current platform supports the audioplayers plugin static final bool mkSupportedPlatform = @@ -140,9 +145,7 @@ class SpotubeAudioPlayer { Stream get currentIndexChangedStream { if (mkSupportedPlatform) { - return _mkPlayer!.streams.playlist - .map((event) => event.index) - .asBroadcastStream(); + return _mkPlayer!.indexChangeStream; } else { return _justAudio!.sequenceStateStream .map((event) => event?.currentIndex ?? -1) @@ -179,7 +182,7 @@ class SpotubeAudioPlayer { bool get hasSource { if (mkSupportedPlatform) { - return _mkPlayer!.state.playlist.medias.isNotEmpty; + return _mkPlayer!.playlist.medias.isNotEmpty; } else { return _justAudio!.audioSource != null; } @@ -378,7 +381,7 @@ class SpotubeAudioPlayer { List get sources { if (mkSupportedPlatform) { - return _mkPlayer!.state.playlist.medias.map((e) => e.uri).toList(); + return _mkPlayer!.playlist.medias.map((e) => e.uri).toList(); } else { return (_justAudio!.audioSource as ja.ConcatenatingAudioSource) .children @@ -389,7 +392,7 @@ class SpotubeAudioPlayer { int get currentIndex { if (mkSupportedPlatform) { - return _mkPlayer!.state.playlist.index; + return _mkPlayer!.playlist.index; } else { return _justAudio!.sequenceState?.currentIndex ?? -1; } @@ -455,44 +458,40 @@ class SpotubeAudioPlayer { final oldSourceIndex = sources.indexOf(oldSource); if (oldSourceIndex == -1) return; - if (mkSupportedPlatform) { - final sourcesCp = sources.toList(); - sourcesCp[oldSourceIndex] = newSource; + // if (mkSupportedPlatform) { + // final sourcesCp = sources.toList(); + // sourcesCp[oldSourceIndex] = newSource; - await _mkPlayer!.open( - mk.Playlist( - sourcesCp.map(mk.Media.new).toList(), - index: currentIndex, - ), - play: false, - ); - if (exclusive) await jumpTo(oldSourceIndex); - } else { - await addTrack(newSource); - await removeTrack(oldSourceIndex); + // await _mkPlayer!.open( + // mk.Playlist( + // sourcesCp.map(mk.Media.new).toList(), + // index: currentIndex, + // ), + // play: false, + // ); + // if (exclusive) await jumpTo(oldSourceIndex); + // } else { + await addTrack(newSource); + await removeTrack(oldSourceIndex); - int newSourceIndex = sources.indexOf(newSource); - while (newSourceIndex == -1) { - await Future.delayed(const Duration(milliseconds: 100)); - newSourceIndex = sources.indexOf(newSource); - } + int newSourceIndex = sources.indexOf(newSource); + while (newSourceIndex == -1) { + await Future.delayed(const Duration(milliseconds: 100)); + newSourceIndex = sources.indexOf(newSource); + } + await moveTrack(newSourceIndex, oldSourceIndex); + newSourceIndex = sources.indexOf(newSource); + while (newSourceIndex != oldSourceIndex) { + await Future.delayed(const Duration(milliseconds: 100)); await moveTrack(newSourceIndex, oldSourceIndex); newSourceIndex = sources.indexOf(newSource); - while (newSourceIndex != oldSourceIndex) { - await Future.delayed(const Duration(milliseconds: 100)); - await moveTrack(newSourceIndex, oldSourceIndex); - newSourceIndex = sources.indexOf(newSource); - } } + // } } Future clearPlaylist() async { if (mkSupportedPlatform) { - await Future.wait( - _mkPlayer!.state.playlist.medias.mapIndexed( - (i, e) async => await _mkPlayer!.remove(i), - ), - ); + _mkPlayer!.stop(); } else { await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear(); } diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart index e8809dd0..b5f9b1cf 100644 --- a/lib/services/audio_player/mk_state_player.dart +++ b/lib/services/audio_player/mk_state_player.dart @@ -1,12 +1,16 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:media_kit/media_kit.dart'; +// ignore: implementation_imports +import 'package:media_kit/src/models/playable.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; /// MediaKit [Player] by default doesn't have a state stream. /// This class adds a state stream to the [Player] class. class MkPlayerWithState extends Player { final StreamController _playerStateStream; + final StreamController _playlistStream; final StreamController _shuffleStream; final StreamController _loopModeStream; @@ -15,10 +19,14 @@ class MkPlayerWithState extends Player { bool _shuffled; PlaylistMode _loopMode; + Playlist? _playlist; + List? _tempMedias; + MkPlayerWithState({super.configuration}) : _playerStateStream = StreamController.broadcast(), _shuffleStream = StreamController.broadcast(), _loopModeStream = StreamController.broadcast(), + _playlistStream = StreamController.broadcast(), _shuffled = false, _loopMode = PlaylistMode.none { _subscriptions = [ @@ -32,8 +40,15 @@ class MkPlayerWithState extends Player { _playerStateStream.add(AudioPlaybackState.paused); } }), - streams.completed.listen((event) { + streams.completed.listen((event) async { _playerStateStream.add(AudioPlaybackState.completed); + if (!event || _playlist == null) return; + + if (loopMode == PlaylistMode.single) { + await super.open(_playlist!.medias[_playlist!.index], play: true); + } else { + await next(); + } }), streams.playlist.listen((event) { if (event.medias.isEmpty) { @@ -45,16 +60,51 @@ class MkPlayerWithState extends Player { bool get shuffled => _shuffled; PlaylistMode get loopMode => _loopMode; + Playlist get playlist => _playlist ?? const Playlist([], index: -1); Stream get playerStateStream => _playerStateStream.stream; Stream get shuffleStream => _shuffleStream.stream; Stream get loopModeStream => _loopModeStream.stream; + Stream get playlistStream => _playlistStream.stream; + Stream get indexChangeStream { + int index = playlist.index; + return playlistStream.map((event) => event.index).where((event) { + if (event != index) { + index = event; + return true; + } + return false; + }); + } + + set playlist(Playlist playlist) { + _playlist = playlist; + _playlistStream.add(playlist); + } @override Future setShuffle(bool shuffle) async { _shuffled = shuffle; await super.setShuffle(shuffle); _shuffleStream.add(shuffle); + if (shuffle) { + _tempMedias = _playlist!.medias; + final active = _playlist!.medias[_playlist!.index]; + final newMedias = _playlist!.medias.toList()..shuffle(); + playlist = _playlist!.copyWith( + medias: newMedias, + index: newMedias.indexOf(active), + ); + } else { + if (_tempMedias == null) return; + playlist = _playlist!.copyWith( + medias: _tempMedias!, + index: _tempMedias?.indexOf( + _playlist!.medias[_playlist!.index], + ), + ); + _tempMedias = null; + } } @override @@ -65,9 +115,14 @@ class MkPlayerWithState extends Player { } Future stop() async { - pause(); + await pause(); + _loopMode = PlaylistMode.none; _shuffled = false; + _playlist = null; + _tempMedias = null; + _playerStateStream.add(AudioPlaybackState.stopped); + for (int i = 0; i < state.playlist.medias.length; i++) { await remove(i); } @@ -80,4 +135,133 @@ class MkPlayerWithState extends Player { } return super.dispose(code: code); } + + @override + Future open( + Playable playable, { + bool play = true, + }) async { + if (playable is Playlist) { + playlist = playable; + super.open(playable.medias[playable.index], play: play); + } + await super.open(playable, play: play); + } + + @override + FutureOr next() { + if (_playlist == null || _playlist!.index + 1 >= _playlist!.medias.length) { + return null; + } + + if (loopMode == PlaylistMode.loop && + _playlist!.index == _playlist!.medias.length - 1) { + playlist = _playlist!.copyWith(index: 0); + } else { + playlist = _playlist!.copyWith(index: _playlist!.index + 1); + } + + return super.open(_playlist!.medias[_playlist!.index], play: true); + } + + @override + FutureOr previous() { + if (_playlist == null || _playlist!.index - 1 < 0) return null; + + if (loopMode == PlaylistMode.loop && _playlist!.index == 0) { + playlist = _playlist!.copyWith(index: _playlist!.medias.length - 1); + } else { + playlist = _playlist!.copyWith(index: _playlist!.index - 1); + } + + return super.open(_playlist!.medias[_playlist!.index], play: true); + } + + @override + FutureOr jump(int index) { + if (_playlist == null || index < 0 || index >= _playlist!.medias.length) { + return null; + } + + playlist = _playlist!.copyWith(index: index); + return super.open(_playlist!.medias[_playlist!.index], play: true); + } + + @override + FutureOr move(int from, int to) { + if (_playlist == null || + from >= _playlist!.medias.length || + to >= _playlist!.medias.length) return null; + + final active = _playlist!.medias[_playlist!.index]; + final newPlaylist = _playlist!.copyWith( + medias: _playlist!.medias.mapIndexed((index, element) { + if (index == from) { + return _playlist!.medias[to]; + } else if (index == to) { + return _playlist!.medias[from]; + } + return element; + }).toList(), + ); + playlist = _playlist!.copyWith( + index: newPlaylist.medias.indexOf(active), + medias: newPlaylist.medias, + ); + } + + @override + FutureOr add(Media media) { + if (_playlist == null) return null; + + playlist = _playlist!.copyWith( + medias: [..._playlist!.medias, media], + ); + + if (shuffled && _tempMedias != null) { + _tempMedias!.add(media); + } + } + + @override + FutureOr remove(int index) async { + if (_playlist == null || index >= _playlist!.medias.length) return null; + + final item = _playlist!.medias.elementAtOrNull(index); + if (shuffled && _tempMedias != null && item != null) { + _tempMedias!.remove(item); + } + + if (_playlist!.index == index) { + final hasNext = _playlist!.index + 1 < _playlist!.medias.length; + final hasPrevious = _playlist!.index - 1 >= 0; + + if (hasNext) { + playlist = _playlist!.copyWith( + index: _playlist!.index + 1, + medias: _playlist!.medias..removeAt(index), + ); + super.open(_playlist!.medias[_playlist!.index], play: true); + } else if (hasPrevious) { + playlist = _playlist!.copyWith( + index: _playlist!.index - 1, + medias: _playlist!.medias..removeAt(index), + ); + super.open(_playlist!.medias[_playlist!.index], play: true); + } else { + playlist = _playlist!.copyWith( + medias: _playlist!.medias..removeAt(index), + index: -1, + ); + await stop(); + } + } else { + final active = _playlist!.medias[_playlist!.index]; + final newMedias = _playlist!.medias..removeAt(index); + playlist = _playlist!.copyWith( + medias: newMedias, + index: newMedias.indexOf(active), + ); + } + } }