From f1080e1675aee1208d05658adfabfbed04ff45b6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 12 May 2023 09:36:03 +0600 Subject: [PATCH] feat(player): add playlist related methods to audio player --- lib/collections/intents.dart | 2 +- lib/components/album/album_card.dart | 2 +- lib/components/player/player_controls.dart | 2 +- lib/components/player/player_overlay.dart | 2 +- lib/components/playlist/playlist_card.dart | 2 +- lib/hooks/use_progress.dart | 2 +- lib/hooks/use_synced_lyrics.dart | 2 +- lib/main.dart | 2 +- lib/provider/playlist_queue_provider.dart | 2 +- .../{ => audio_player}/audio_player.dart | 242 +++++++++++++----- lib/services/audio_player/loop_mode.dart | 53 ++++ .../audio_player/mk_state_player.dart | 46 ++++ lib/services/audio_player/playback_state.dart | 28 ++ .../audio_services/linux_audio_service.dart | 3 +- .../audio_services/mobile_audio_service.dart | 2 +- .../audio_services/windows_audio_service.dart | 3 +- 16 files changed, 318 insertions(+), 77 deletions(-) rename lib/services/{ => audio_player}/audio_player.dart (57%) create mode 100644 lib/services/audio_player/loop_mode.dart create mode 100644 lib/services/audio_player/mk_state_player.dart create mode 100644 lib/services/audio_player/playback_state.dart diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 3c7131862..398bf4a4e 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -6,7 +6,7 @@ import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index ac2fc97f0..128133d6c 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -7,7 +7,7 @@ import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index 3a5c3f2f2..145af90cd 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart'; 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.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/primitive_utils.dart'; class PlayerControls extends HookConsumerWidget { diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index 73b052628..d5ddf0139 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -10,7 +10,7 @@ import 'package:spotube/components/player/player_track_details.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/hooks/use_progress.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; class PlayerOverlay extends HookConsumerWidget { diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index 1fcf88de6..b21996204 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -6,7 +6,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/hooks/use_progress.dart b/lib/hooks/use_progress.dart index 90b256288..997baa234 100644 --- a/lib/hooks/use_progress.dart +++ b/lib/hooks/use_progress.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:tuple/tuple.dart'; Tuple4 useProgress(WidgetRef ref) { diff --git a/lib/hooks/use_synced_lyrics.dart b/lib/hooks/use_synced_lyrics.dart index 1bdbea65c..7a1714737 100644 --- a/lib/hooks/use_synced_lyrics.dart +++ b/lib/hooks/use_synced_lyrics.dart @@ -1,6 +1,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; int useSyncedLyrics( WidgetRef ref, diff --git a/lib/main.dart b/lib/main.dart index f682cfba9..6b066449a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,7 +24,7 @@ import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/pocketbase.dart'; import 'package:spotube/services/youtube.dart'; import 'package:spotube/themes/theme.dart'; diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index 33b013c40..1a9e4b8af 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/track.dart'; 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.dart'; +import 'package:spotube/services/audio_player/audio_player.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'; diff --git a/lib/services/audio_player.dart b/lib/services/audio_player/audio_player.dart similarity index 57% rename from lib/services/audio_player.dart rename to lib/services/audio_player/audio_player.dart index 519764b45..511e4b5ef 100644 --- a/lib/services/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,37 +1,16 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:media_kit/media_kit.dart' as mk; import 'package:just_audio/just_audio.dart' as ja; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/services/audio_player/loop_mode.dart'; +import 'package:spotube/services/audio_player/mk_state_player.dart'; +import 'package:spotube/services/audio_player/playback_state.dart'; final audioPlayer = SpotubeAudioPlayer(); -enum AudioPlaybackState { - playing, - paused, - completed, - buffering, - stopped; - - static AudioPlaybackState fromJaPlayerState(ja.PlayerState state) { - if (state.playing) { - return AudioPlaybackState.playing; - } - - switch (state.processingState) { - case ja.ProcessingState.idle: - return AudioPlaybackState.stopped; - case ja.ProcessingState.ready: - return AudioPlaybackState.paused; - case ja.ProcessingState.completed: - return AudioPlaybackState.completed; - case ja.ProcessingState.loading: - case ja.ProcessingState.buffering: - return AudioPlaybackState.buffering; - } - } -} - class SpotubeAudioPlayer { final MkPlayerWithState? _mkPlayer; final ja.AudioPlayer? _justAudio; @@ -84,6 +63,18 @@ class SpotubeAudioPlayer { } } + /// Stream that emits when the player is almost (80%) complete + Stream get almostCompleteStream { + 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 * .8).toInt(); + }).asBroadcastStream(); + } + Stream get playingStream { if (mkSupportedPlatform) { return _mkPlayer!.streams.playing.asBroadcastStream(); @@ -250,6 +241,8 @@ class SpotubeAudioPlayer { } Future stop() async { + _mkLooped = PlaybackLoopMode.none; + _mkShuffled = false; await _mkPlayer?.pause(); await _justAudio?.stop(); } @@ -273,45 +266,164 @@ class SpotubeAudioPlayer { await _mkPlayer?.dispose(); await _justAudio?.dispose(); } -} -/// MediaKit [mk.Player] by default doesn't have a state stream. -class MkPlayerWithState extends mk.Player { - final StreamController _playerStateStream; - - late final List _subscriptions; - - MkPlayerWithState({super.configuration}) - : _playerStateStream = StreamController.broadcast() { - _subscriptions = [ - streams.buffering.listen((event) { - _playerStateStream.add(AudioPlaybackState.buffering); - }), - streams.playing.listen((playing) { - if (playing) { - _playerStateStream.add(AudioPlaybackState.playing); - } else { - _playerStateStream.add(AudioPlaybackState.paused); - } - }), - streams.completed.listen((event) { - _playerStateStream.add(AudioPlaybackState.completed); - }), - streams.playlist.listen((event) { - if (event.medias.isEmpty) { - _playerStateStream.add(AudioPlaybackState.stopped); - } - }), - ]; - } - - Stream get playerStateStream => _playerStateStream.stream; - - @override - FutureOr dispose({int code = 0}) { - for (var element in _subscriptions) { - element.cancel(); - } - return super.dispose(code: code); + // Playlist related + + Future openPlaylist( + List tracks, { + bool autoPlay = true, + int initialIndex = 0, + }) async { + assert(tracks.isNotEmpty); + assert(initialIndex <= tracks.length - 1); + if (mkSupportedPlatform) { + await _mkPlayer!.open( + mk.Playlist( + tracks.map((e) => mk.Media(e)).toList(), + index: initialIndex, + ), + play: autoPlay, + ); + } else { + await _justAudio!.setAudioSource( + ja.ConcatenatingAudioSource( + useLazyPreparation: true, + children: + tracks.map((e) => ja.AudioSource.uri(Uri.parse(e))).toList(), + ), + preload: true, + initialIndex: initialIndex, + ); + if (autoPlay) { + await _justAudio!.play(); + } + } + } + + 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(); + } + } + + bool tracksExistsInPlaylist(List tracks) { + return resolveTracksForSource(tracks).length == tracks.length; + } + + int get currentIndex { + if (mkSupportedPlatform) { + return _mkPlayer!.state.playlist.index; + } else { + return _justAudio!.sequenceState!.currentIndex; + } + } + + Future skipToNext() async { + if (mkSupportedPlatform) { + await _mkPlayer!.next(); + } else { + await _justAudio!.seekToNext(); + } + } + + Future skipToPrevious() async { + if (mkSupportedPlatform) { + await _mkPlayer!.previous(); + } else { + await _justAudio!.seekToPrevious(); + } + } + + Future skipToIndex(int index) async { + if (mkSupportedPlatform) { + await _mkPlayer!.jump(index); + } else { + await _justAudio!.seek(Duration.zero, index: index); + } + } + + Future addTrack(String url) async { + final urlType = _resolveUrlType(url); + if (mkSupportedPlatform && urlType is mk.Media) { + await _mkPlayer!.add(urlType); + } else { + await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) + .add(urlType as ja.AudioSource); + } + } + + Future removeTrack(int index) async { + if (mkSupportedPlatform) { + await _mkPlayer!.remove(index); + } else { + await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) + .removeAt(index); + } + } + + Future moveTrack(int from, int to) async { + if (mkSupportedPlatform) { + await _mkPlayer!.move(from, to); + } else { + await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) + .move(from, to); + } + } + + Future clearPlaylist() async { + if (mkSupportedPlatform) { + await Future.wait( + _mkPlayer!.state.playlist.medias.mapIndexed( + (i, e) async => await _mkPlayer!.remove(i), + ), + ); + } else { + await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear(); + } + } + + bool _mkShuffled = false; + + Future setShuffle(bool shuffle) async { + if (mkSupportedPlatform) { + await _mkPlayer!.setShuffle(shuffle); + _mkShuffled = shuffle; + } else { + await _justAudio!.setShuffleModeEnabled(shuffle); + } + } + + Future isShuffled() async { + if (mkSupportedPlatform) { + return _mkShuffled; + } else { + return _justAudio!.shuffleModeEnabled; + } + } + + PlaybackLoopMode _mkLooped = PlaybackLoopMode.none; + + Future setLoopMode(PlaybackLoopMode loop) async { + if (mkSupportedPlatform) { + await _mkPlayer!.setPlaylistMode(loop.toPlaylistMode()); + _mkLooped = loop; + } else { + await _justAudio!.setLoopMode(loop.toLoopMode()); + } + } + + Future getLoopMode() async { + if (mkSupportedPlatform) { + return _mkLooped; + } else { + return PlaybackLoopMode.fromLoopMode(_justAudio!.loopMode); + } } } diff --git a/lib/services/audio_player/loop_mode.dart b/lib/services/audio_player/loop_mode.dart new file mode 100644 index 000000000..7f767c44a --- /dev/null +++ b/lib/services/audio_player/loop_mode.dart @@ -0,0 +1,53 @@ +import 'package:media_kit/media_kit.dart'; +import 'package:just_audio/just_audio.dart'; + +/// An unified loop mode for both [LoopMode] and [PlaylistMode] +enum PlaybackLoopMode { + all, + one, + none; + + static PlaybackLoopMode fromLoopMode(LoopMode loopMode) { + switch (loopMode) { + case LoopMode.all: + return PlaybackLoopMode.all; + case LoopMode.one: + return PlaybackLoopMode.one; + case LoopMode.off: + return PlaybackLoopMode.none; + } + } + + LoopMode toLoopMode() { + switch (this) { + case PlaybackLoopMode.all: + return LoopMode.all; + case PlaybackLoopMode.one: + return LoopMode.one; + case PlaybackLoopMode.none: + return LoopMode.off; + } + } + + static PlaybackLoopMode fromPlaylistMode(PlaylistMode mode) { + switch (mode) { + case PlaylistMode.single: + return PlaybackLoopMode.one; + case PlaylistMode.loop: + return PlaybackLoopMode.all; + case PlaylistMode.none: + return PlaybackLoopMode.none; + } + } + + PlaylistMode toPlaylistMode() { + switch (this) { + case PlaybackLoopMode.all: + return PlaylistMode.loop; + case PlaybackLoopMode.one: + return PlaylistMode.single; + case PlaybackLoopMode.none: + return PlaylistMode.none; + } + } +} diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart new file mode 100644 index 000000000..bccd04db6 --- /dev/null +++ b/lib/services/audio_player/mk_state_player.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:media_kit/media_kit.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; + + late final List _subscriptions; + + MkPlayerWithState({super.configuration}) + : _playerStateStream = StreamController.broadcast() { + _subscriptions = [ + streams.buffering.listen((event) { + _playerStateStream.add(AudioPlaybackState.buffering); + }), + streams.playing.listen((playing) { + if (playing) { + _playerStateStream.add(AudioPlaybackState.playing); + } else { + _playerStateStream.add(AudioPlaybackState.paused); + } + }), + streams.completed.listen((event) { + _playerStateStream.add(AudioPlaybackState.completed); + }), + streams.playlist.listen((event) { + if (event.medias.isEmpty) { + _playerStateStream.add(AudioPlaybackState.stopped); + } + }), + ]; + } + + Stream get playerStateStream => _playerStateStream.stream; + + @override + FutureOr dispose({int code = 0}) { + for (var element in _subscriptions) { + element.cancel(); + } + return super.dispose(code: code); + } +} diff --git a/lib/services/audio_player/playback_state.dart b/lib/services/audio_player/playback_state.dart new file mode 100644 index 000000000..0bdae439b --- /dev/null +++ b/lib/services/audio_player/playback_state.dart @@ -0,0 +1,28 @@ +import 'package:just_audio/just_audio.dart'; + +/// An unified playback state enum +enum AudioPlaybackState { + playing, + paused, + completed, + buffering, + stopped; + + static AudioPlaybackState fromJaPlayerState(PlayerState state) { + if (state.playing) { + return AudioPlaybackState.playing; + } + + switch (state.processingState) { + case ProcessingState.idle: + return AudioPlaybackState.stopped; + case ProcessingState.ready: + return AudioPlaybackState.paused; + case ProcessingState.completed: + return AudioPlaybackState.completed; + case ProcessingState.loading: + case ProcessingState.buffering: + return AudioPlaybackState.buffering; + } + } +} diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart index 91ea3e6ec..c8d986fa6 100644 --- a/lib/services/audio_services/linux_audio_service.dart +++ b/lib/services/audio_services/linux_audio_service.dart @@ -6,7 +6,8 @@ import 'package:mpris_service/mpris_service.dart'; 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.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class LinuxAudioService { diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 6ac7c08c1..233fa3d0c 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -3,7 +3,7 @@ import 'dart:async'; 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.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; class MobileAudioService extends BaseAudioHandler { AudioSession? session; diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index e17a0b566..ead9b5355 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -4,7 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:smtc_windows/smtc_windows.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; -import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class WindowsAudioService {