feat(player): add playlist related methods to audio player

This commit is contained in:
Kingkor Roy Tirtho 2023-05-12 09:36:03 +06:00
parent 06f6adc69c
commit f1080e1675
16 changed files with 316 additions and 75 deletions

View File

@ -6,7 +6,7 @@ import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playlist_queue_provider.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:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';

View File

@ -7,7 +7,7 @@ import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/spotify_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/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';

View File

@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/use_progress.dart'; import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playlist_queue_provider.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'; import 'package:spotube/utils/primitive_utils.dart';
class PlayerControls extends HookConsumerWidget { class PlayerControls extends HookConsumerWidget {

View File

@ -10,7 +10,7 @@ import 'package:spotube/components/player/player_track_details.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';
import 'package:spotube/hooks/use_progress.dart'; import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/provider/playlist_queue_provider.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'; import 'package:spotube/utils/service_utils.dart';
class PlayerOverlay extends HookConsumerWidget { class PlayerOverlay extends HookConsumerWidget {

View File

@ -6,7 +6,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/spotify_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/services/queries/queries.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/playlist_queue_provider.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'; import 'package:tuple/tuple.dart';
Tuple4<double, Duration, Duration, double> useProgress(WidgetRef ref) { Tuple4<double, Duration, Duration, double> useProgress(WidgetRef ref) {

View File

@ -1,6 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.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( int useSyncedLyrics(
WidgetRef ref, WidgetRef ref,

View File

@ -24,7 +24,7 @@ import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/downloader_provider.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/user_preferences_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/pocketbase.dart';
import 'package:spotube/services/youtube.dart'; import 'package:spotube/services/youtube.dart';
import 'package:spotube/themes/theme.dart'; import 'package:spotube/themes/theme.dart';

View File

@ -11,7 +11,7 @@ import 'package:spotube/extensions/track.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/user_preferences_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/services/audio_services/audio_services.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';

View File

@ -1,37 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:media_kit/media_kit.dart' as mk; import 'package:media_kit/media_kit.dart' as mk;
import 'package:just_audio/just_audio.dart' as ja; import 'package:just_audio/just_audio.dart' as ja;
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; 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(); 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 { class SpotubeAudioPlayer {
final MkPlayerWithState? _mkPlayer; final MkPlayerWithState? _mkPlayer;
final ja.AudioPlayer? _justAudio; final ja.AudioPlayer? _justAudio;
@ -84,6 +63,18 @@ class SpotubeAudioPlayer {
} }
} }
/// Stream that emits when the player is almost (80%) complete
Stream<void> 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<bool> get playingStream { Stream<bool> get playingStream {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
return _mkPlayer!.streams.playing.asBroadcastStream(); return _mkPlayer!.streams.playing.asBroadcastStream();
@ -250,6 +241,8 @@ class SpotubeAudioPlayer {
} }
Future<void> stop() async { Future<void> stop() async {
_mkLooped = PlaybackLoopMode.none;
_mkShuffled = false;
await _mkPlayer?.pause(); await _mkPlayer?.pause();
await _justAudio?.stop(); await _justAudio?.stop();
} }
@ -273,45 +266,164 @@ class SpotubeAudioPlayer {
await _mkPlayer?.dispose(); await _mkPlayer?.dispose();
await _justAudio?.dispose(); await _justAudio?.dispose();
} }
}
/// MediaKit [mk.Player] by default doesn't have a state stream. // Playlist related
class MkPlayerWithState extends mk.Player {
final StreamController<AudioPlaybackState> _playerStateStream;
late final List<StreamSubscription> _subscriptions; Future<void> openPlaylist(
List<String> tracks, {
MkPlayerWithState({super.configuration}) bool autoPlay = true,
: _playerStateStream = StreamController.broadcast() { int initialIndex = 0,
_subscriptions = [ }) async {
streams.buffering.listen((event) { assert(tracks.isNotEmpty);
_playerStateStream.add(AudioPlaybackState.buffering); assert(initialIndex <= tracks.length - 1);
}), if (mkSupportedPlatform) {
streams.playing.listen((playing) { await _mkPlayer!.open(
if (playing) { mk.Playlist(
_playerStateStream.add(AudioPlaybackState.playing); tracks.map((e) => mk.Media(e)).toList(),
} else { index: initialIndex,
_playerStateStream.add(AudioPlaybackState.paused); ),
} play: autoPlay,
}), );
streams.completed.listen((event) { } else {
_playerStateStream.add(AudioPlaybackState.completed); await _justAudio!.setAudioSource(
}), ja.ConcatenatingAudioSource(
streams.playlist.listen((event) { useLazyPreparation: true,
if (event.medias.isEmpty) { children:
_playerStateStream.add(AudioPlaybackState.stopped); tracks.map((e) => ja.AudioSource.uri(Uri.parse(e))).toList(),
} ),
}), preload: true,
]; initialIndex: initialIndex,
} );
if (autoPlay) {
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream; await _justAudio!.play();
}
@override }
FutureOr<void> dispose({int code = 0}) { }
for (var element in _subscriptions) {
element.cancel(); List<SpotubeTrack> resolveTracksForSource(List<SpotubeTrack> 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<SpotubeTrack> tracks) {
return resolveTracksForSource(tracks).length == tracks.length;
}
int get currentIndex {
if (mkSupportedPlatform) {
return _mkPlayer!.state.playlist.index;
} else {
return _justAudio!.sequenceState!.currentIndex;
}
}
Future<void> skipToNext() async {
if (mkSupportedPlatform) {
await _mkPlayer!.next();
} else {
await _justAudio!.seekToNext();
}
}
Future<void> skipToPrevious() async {
if (mkSupportedPlatform) {
await _mkPlayer!.previous();
} else {
await _justAudio!.seekToPrevious();
}
}
Future<void> skipToIndex(int index) async {
if (mkSupportedPlatform) {
await _mkPlayer!.jump(index);
} else {
await _justAudio!.seek(Duration.zero, index: index);
}
}
Future<void> 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<void> removeTrack(int index) async {
if (mkSupportedPlatform) {
await _mkPlayer!.remove(index);
} else {
await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
.removeAt(index);
}
}
Future<void> moveTrack(int from, int to) async {
if (mkSupportedPlatform) {
await _mkPlayer!.move(from, to);
} else {
await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
.move(from, to);
}
}
Future<void> 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<void> setShuffle(bool shuffle) async {
if (mkSupportedPlatform) {
await _mkPlayer!.setShuffle(shuffle);
_mkShuffled = shuffle;
} else {
await _justAudio!.setShuffleModeEnabled(shuffle);
}
}
Future<bool> isShuffled() async {
if (mkSupportedPlatform) {
return _mkShuffled;
} else {
return _justAudio!.shuffleModeEnabled;
}
}
PlaybackLoopMode _mkLooped = PlaybackLoopMode.none;
Future<void> setLoopMode(PlaybackLoopMode loop) async {
if (mkSupportedPlatform) {
await _mkPlayer!.setPlaylistMode(loop.toPlaylistMode());
_mkLooped = loop;
} else {
await _justAudio!.setLoopMode(loop.toLoopMode());
}
}
Future<PlaybackLoopMode> getLoopMode() async {
if (mkSupportedPlatform) {
return _mkLooped;
} else {
return PlaybackLoopMode.fromLoopMode(_justAudio!.loopMode);
} }
return super.dispose(code: code);
} }
} }

View File

@ -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;
}
}
}

View File

@ -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<AudioPlaybackState> _playerStateStream;
late final List<StreamSubscription> _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<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
@override
FutureOr<void> dispose({int code = 0}) {
for (var element in _subscriptions) {
element.cancel();
}
return super.dispose(code: code);
}
}

View File

@ -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;
}
}
}

View File

@ -6,7 +6,8 @@ import 'package:mpris_service/mpris_service.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/playlist_queue_provider.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'; import 'package:spotube/utils/type_conversion_utils.dart';
class LinuxAudioService { class LinuxAudioService {

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart'; import 'package:audio_session/audio_session.dart';
import 'package:spotube/provider/playlist_queue_provider.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 { class MobileAudioService extends BaseAudioHandler {
AudioSession? session; AudioSession? session;

View File

@ -4,7 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:smtc_windows/smtc_windows.dart'; import 'package:smtc_windows/smtc_windows.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/playlist_queue_provider.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'; import 'package:spotube/utils/type_conversion_utils.dart';
class WindowsAudioService { class WindowsAudioService {