diff --git a/lib/hooks/use_init_sys_tray.dart b/lib/hooks/use_init_sys_tray.dart index 916b666f..e9aa05b6 100644 --- a/lib/hooks/use_init_sys_tray.dart +++ b/lib/hooks/use_init_sys_tray.dart @@ -23,7 +23,7 @@ void useInitSysTray(WidgetRef ref) { } final enabled = !playlist.isFetching; systemTray.value = await DesktopTools.createSystemTrayMenu( - title: "Spotube", + title: DesktopTools.platform.isLinux ? "" : "Spotube", iconPath: "assets/spotube-logo.png", windowsIconPath: "assets/spotube-logo.ico", items: [ diff --git a/lib/hooks/use_progress.dart b/lib/hooks/use_progress.dart index cf674feb..b1d07a85 100644 --- a/lib/hooks/use_progress.dart +++ b/lib/hooks/use_progress.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index de5d5d45..f3d63a45 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -25,7 +25,8 @@ import 'package:spotube/utils/type_conversion_utils.dart'; /// * [ ] Remove track /// * [ ] Reorder track /// * [ ] Caching and loading of cache of tracks -/// * [ ] Shuffling and loop => playlist, track, none +/// * [ ] Shuffling +/// * [x] loop => playlist, track, none /// * [ ] Alternative Track Source /// * [x] Blacklisting of tracks and artist /// diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index d03ba188..e2e7a06d 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,161 +1,35 @@ +import 'package:catcher/catcher.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:spotube/services/audio_player/mk_state_player.dart'; +import 'package:just_audio/just_audio.dart' as ja; 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; -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(); +part 'audio_players_streams_mixin.dart'; +part 'audio_player_impl.dart'; -class SpotubeAudioPlayer { +abstract class AudioPlayerInterface { final MkPlayerWithState? _mkPlayer; final ja.AudioPlayer? _justAudio; - SpotubeAudioPlayer() - : _mkPlayer = mkSupportedPlatform ? MkPlayerWithState() : null, - _justAudio = !mkSupportedPlatform ? ja.AudioPlayer() : null { + AudioPlayerInterface() + : _mkPlayer = _mkSupportedPlatform ? MkPlayerWithState() : 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 = + static final bool _mkSupportedPlatform = DesktopTools.platform.isWindows || DesktopTools.platform.isLinux; - // stream getters - Stream get durationStream { - if (mkSupportedPlatform) { - return _mkPlayer!.streams.duration.asBroadcastStream(); - } else { - return _justAudio!.durationStream - .where((event) => event != null) - .map((event) => event!) - .asBroadcastStream(); - } - } - - Stream get positionStream { - if (mkSupportedPlatform) { - return _mkPlayer!.streams.position.asBroadcastStream(); - } else { - return _justAudio!.positionStream.asBroadcastStream(); - } - } - - Stream get bufferedPositionStream { - if (mkSupportedPlatform) { - // audioplayers doesn't have the capability to get buffered position - return _mkPlayer!.streams.buffer.asBroadcastStream(); - } else { - return _justAudio!.bufferedPositionStream.asBroadcastStream(); - } - } - - Stream get completedStream { - if (mkSupportedPlatform) { - return _mkPlayer!.streams.completed.asBroadcastStream(); - } else { - return _justAudio!.playerStateStream - .where( - (event) => event.processingState == ja.ProcessingState.completed) - .asBroadcastStream(); - } - } - - /// Stream that emits when the player is almost (%) complete - Stream percentCompletedStream(double percent) { - return positionStream - .asyncMap( - (position) async => (await duration)?.inSeconds == 0 - ? 0 - : (position.inSeconds / - ((await duration)?.inSeconds ?? 100) * - 100) - .toInt(), - ) - .where((event) => event >= percent) - .asBroadcastStream(); - } - - Stream get playingStream { - if (mkSupportedPlatform) { - return _mkPlayer!.streams.playing.asBroadcastStream(); - } else { - return _justAudio!.playingStream.asBroadcastStream(); - } - } - - Stream get shuffledStream { - if (mkSupportedPlatform) { - return _mkPlayer!.shuffleStream.asBroadcastStream(); - } else { - return _justAudio!.shuffleModeEnabledStream.asBroadcastStream(); - } - } - - Stream get loopModeStream { - if (mkSupportedPlatform) { - return _mkPlayer!.loopModeStream - .map(PlaybackLoopMode.fromPlaylistMode) - .asBroadcastStream(); - } else { - return _justAudio!.loopModeStream - .map(PlaybackLoopMode.fromLoopMode) - .asBroadcastStream(); - } - } - - Stream get volumeStream { - if (mkSupportedPlatform) { - return _mkPlayer!.streams.volume - .map((event) => event / 100) - .asBroadcastStream(); - } else { - return _justAudio!.volumeStream.asBroadcastStream(); - } - } - - Stream get bufferingStream { - if (mkSupportedPlatform) { - return Stream.value(false).asBroadcastStream(); - } else { - return _justAudio!.playerStateStream - .map( - (event) => - event.processingState == ja.ProcessingState.buffering || - event.processingState == ja.ProcessingState.loading, - ) - .asBroadcastStream(); - } - } - - Stream get playerStateStream { - if (mkSupportedPlatform) { - return _mkPlayer!.playerStateStream.asBroadcastStream(); - } else { - return _justAudio!.playerStateStream - .map(AudioPlaybackState.fromJaPlayerState) - .asBroadcastStream(); - } - } - - Stream get currentIndexChangedStream { - if (mkSupportedPlatform) { - return _mkPlayer!.indexChangeStream; - } else { - return _justAudio!.sequenceStateStream - .map((event) => event?.currentIndex ?? -1) - .asBroadcastStream(); - } - } - - // regular info getter + bool get mkSupportedPlatform => _mkSupportedPlatform; Future get duration async { if (mkSupportedPlatform) { @@ -257,251 +131,4 @@ class SpotubeAudioPlayer { _justAudio!.processingState == ja.ProcessingState.loading; } } - - Object _resolveUrlType(String url) { - if (mkSupportedPlatform) { - return mk.Media(url); - } else { - if (url.startsWith("https")) { - return ja.AudioSource.uri(Uri.parse(url)); - } else { - return ja.AudioSource.file(url); - } - } - } - - Future preload(String url) async { - throw UnimplementedError(); - // final urlType = _resolveUrlType(url); - // if (mkSupportedPlatform && urlType is ap.Source) { - // // audioplayers doesn't have the capability to preload - // return; - // } else { - // return; - // } - } - - Future play(String url) async { - final urlType = _resolveUrlType(url); - if (mkSupportedPlatform && urlType is mk.Media) { - await _mkPlayer?.open(urlType, play: true); - } else { - if (_justAudio?.audioSource is ja.ProgressiveAudioSource && - (_justAudio?.audioSource as ja.ProgressiveAudioSource) - .uri - .toString() == - url) { - await _justAudio?.play(); - } else { - await _justAudio?.stop(); - await _justAudio?.setAudioSource( - urlType as ja.AudioSource, - preload: true, - ); - await _justAudio?.play(); - } - } - } - - Future pause() async { - await _mkPlayer?.pause(); - await _justAudio?.pause(); - } - - Future resume() async { - await _mkPlayer?.play(); - await _justAudio?.play(); - } - - Future stop() async { - await _mkPlayer?.stop(); - await _justAudio?.stop(); - } - - Future seek(Duration position) async { - await _mkPlayer?.seek(position); - await _justAudio?.seek(position); - } - - /// Volume is between 0 and 1 - Future setVolume(double volume) async { - assert(volume >= 0 && volume <= 1); - await _mkPlayer?.setVolume(volume * 100); - await _justAudio?.setVolume(volume); - } - - Future setSpeed(double speed) async { - await _mkPlayer?.setRate(speed); - await _justAudio?.setSpeed(speed); - } - - Future dispose() async { - await _mkPlayer?.dispose(); - await _justAudio?.dispose(); - } - - // 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(mk.Media.new).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) { - 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!.playlist.medias.map((e) => e.uri).toList(); - } else { - return _justAudio!.sequenceState?.effectiveSequence - .map((e) => (e as ja.UriAudioSource).uri.toString()) - .toList() ?? - []; - } - } - - int get currentIndex { - if (mkSupportedPlatform) { - return _mkPlayer!.playlist.index; - } else { - return _justAudio!.sequenceState?.currentIndex ?? -1; - } - } - - 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 jumpTo(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 replaceSource( - String oldSource, - String newSource, { - bool exclusive = false, - }) async { - final oldSourceIndex = sources.indexOf(oldSource); - if (oldSourceIndex == -1) return; - - if (mkSupportedPlatform) { - _mkPlayer!.replace(oldSource, newSource); - } 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); - } - 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) { - _mkPlayer!.stop(); - } else { - await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear(); - } - } - - Future setShuffle(bool shuffle) async { - if (mkSupportedPlatform) { - await _mkPlayer!.setShuffle(shuffle); - } else { - await _justAudio!.setShuffleModeEnabled(shuffle); - } - } - - Future setLoopMode(PlaybackLoopMode loop) async { - if (mkSupportedPlatform) { - await _mkPlayer!.setPlaylistMode(loop.toPlaylistMode()); - } else { - await _justAudio!.setLoopMode(loop.toLoopMode()); - } - } } diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart new file mode 100644 index 00000000..455dde3b --- /dev/null +++ b/lib/services/audio_player/audio_player_impl.dart @@ -0,0 +1,253 @@ +part of 'audio_player.dart'; + +final audioPlayer = SpotubeAudioPlayer(); + +class SpotubeAudioPlayer extends AudioPlayerInterface + with SpotubeAudioPlayersStreams { + Object _resolveUrlType(String url) { + if (mkSupportedPlatform) { + return mk.Media(url); + } else { + if (url.startsWith("https")) { + return ja.AudioSource.uri(Uri.parse(url)); + } else { + return ja.AudioSource.file(url); + } + } + } + + Future preload(String url) async { + throw UnimplementedError(); + // final urlType = _resolveUrlType(url); + // if (mkSupportedPlatform && urlType is ap.Source) { + // // audioplayers doesn't have the capability to preload + // return; + // } else { + // return; + // } + } + + Future play(String url) async { + final urlType = _resolveUrlType(url); + if (mkSupportedPlatform && urlType is mk.Media) { + await _mkPlayer?.open(urlType, play: true); + } else { + if (_justAudio?.audioSource is ja.ProgressiveAudioSource && + (_justAudio?.audioSource as ja.ProgressiveAudioSource) + .uri + .toString() == + url) { + await _justAudio?.play(); + } else { + await _justAudio?.stop(); + await _justAudio?.setAudioSource( + urlType as ja.AudioSource, + preload: true, + ); + await _justAudio?.play(); + } + } + } + + Future pause() async { + await _mkPlayer?.pause(); + await _justAudio?.pause(); + } + + Future resume() async { + await _mkPlayer?.play(); + await _justAudio?.play(); + } + + Future stop() async { + await _mkPlayer?.stop(); + await _justAudio?.stop(); + } + + Future seek(Duration position) async { + await _mkPlayer?.seek(position); + await _justAudio?.seek(position); + } + + /// Volume is between 0 and 1 + Future setVolume(double volume) async { + assert(volume >= 0 && volume <= 1); + await _mkPlayer?.setVolume(volume * 100); + await _justAudio?.setVolume(volume); + } + + Future setSpeed(double speed) async { + await _mkPlayer?.setRate(speed); + await _justAudio?.setSpeed(speed); + } + + Future dispose() async { + await _mkPlayer?.dispose(); + await _justAudio?.dispose(); + } + + // 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(mk.Media.new).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) { + 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!.playlist.medias.map((e) => e.uri).toList(); + } else { + return _justAudio!.sequenceState?.effectiveSequence + .map((e) => (e as ja.UriAudioSource).uri.toString()) + .toList() ?? + []; + } + } + + int get currentIndex { + if (mkSupportedPlatform) { + return _mkPlayer!.playlist.index; + } else { + return _justAudio!.sequenceState?.currentIndex ?? -1; + } + } + + 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 jumpTo(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 replaceSource( + String oldSource, + String newSource, { + bool exclusive = false, + }) async { + final oldSourceIndex = sources.indexOf(oldSource); + if (oldSourceIndex == -1) return; + + if (mkSupportedPlatform) { + _mkPlayer!.replace(oldSource, newSource); + } 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); + } + 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) { + _mkPlayer!.stop(); + } else { + await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear(); + } + } + + Future setShuffle(bool shuffle) async { + if (mkSupportedPlatform) { + await _mkPlayer!.setShuffle(shuffle); + } else { + await _justAudio!.setShuffleModeEnabled(shuffle); + } + } + + Future setLoopMode(PlaybackLoopMode loop) async { + if (mkSupportedPlatform) { + await _mkPlayer!.setPlaylistMode(loop.toPlaylistMode()); + } else { + await _justAudio!.setLoopMode(loop.toLoopMode()); + } + } +} diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart new file mode 100644 index 00000000..dc5654ed --- /dev/null +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -0,0 +1,130 @@ +part of 'audio_player.dart'; + +mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { + // stream getters + Stream get durationStream { + if (mkSupportedPlatform) { + return _mkPlayer!.streams.duration.asBroadcastStream(); + } else { + return _justAudio!.durationStream + .where((event) => event != null) + .map((event) => event!) + .asBroadcastStream(); + } + } + + Stream get positionStream { + if (mkSupportedPlatform) { + return _mkPlayer!.streams.position.asBroadcastStream(); + } else { + return _justAudio!.positionStream.asBroadcastStream(); + } + } + + Stream get bufferedPositionStream { + if (mkSupportedPlatform) { + // audioplayers doesn't have the capability to get buffered position + return _mkPlayer!.streams.buffer.asBroadcastStream(); + } else { + return _justAudio!.bufferedPositionStream.asBroadcastStream(); + } + } + + Stream get completedStream { + if (mkSupportedPlatform) { + return _mkPlayer!.streams.completed.asBroadcastStream(); + } else { + return _justAudio!.playerStateStream + .where( + (event) => event.processingState == ja.ProcessingState.completed) + .asBroadcastStream(); + } + } + + /// Stream that emits when the player is almost (%) complete + Stream percentCompletedStream(double percent) { + return positionStream + .asyncMap( + (position) async => (await duration)?.inSeconds == 0 + ? 0 + : (position.inSeconds / + ((await duration)?.inSeconds ?? 100) * + 100) + .toInt(), + ) + .where((event) => event >= percent) + .asBroadcastStream(); + } + + Stream get playingStream { + if (mkSupportedPlatform) { + return _mkPlayer!.streams.playing.asBroadcastStream(); + } else { + return _justAudio!.playingStream.asBroadcastStream(); + } + } + + Stream get shuffledStream { + if (mkSupportedPlatform) { + return _mkPlayer!.shuffleStream.asBroadcastStream(); + } else { + return _justAudio!.shuffleModeEnabledStream.asBroadcastStream(); + } + } + + Stream get loopModeStream { + if (mkSupportedPlatform) { + return _mkPlayer!.loopModeStream + .map(PlaybackLoopMode.fromPlaylistMode) + .asBroadcastStream(); + } else { + return _justAudio!.loopModeStream + .map(PlaybackLoopMode.fromLoopMode) + .asBroadcastStream(); + } + } + + Stream get volumeStream { + if (mkSupportedPlatform) { + return _mkPlayer!.streams.volume + .map((event) => event / 100) + .asBroadcastStream(); + } else { + return _justAudio!.volumeStream.asBroadcastStream(); + } + } + + Stream get bufferingStream { + if (mkSupportedPlatform) { + return Stream.value(false).asBroadcastStream(); + } else { + return _justAudio!.playerStateStream + .map( + (event) => + event.processingState == ja.ProcessingState.buffering || + event.processingState == ja.ProcessingState.loading, + ) + .asBroadcastStream(); + } + } + + Stream get playerStateStream { + if (mkSupportedPlatform) { + return _mkPlayer!.playerStateStream.asBroadcastStream(); + } else { + return _justAudio!.playerStateStream + .map(AudioPlaybackState.fromJaPlayerState) + .asBroadcastStream(); + } + } + + Stream get currentIndexChangedStream { + if (mkSupportedPlatform) { + return _mkPlayer!.indexChangeStream; + } else { + return _justAudio!.sequenceStateStream + .map((event) => event?.currentIndex ?? -1) + .asBroadcastStream(); + } + } +}