mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat(player): add playlist related methods to audio player
This commit is contained in:
parent
06f6adc69c
commit
f1080e1675
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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<double, Duration, Duration, double> useProgress(WidgetRef ref) {
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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<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 {
|
||||
if (mkSupportedPlatform) {
|
||||
return _mkPlayer!.streams.playing.asBroadcastStream();
|
||||
@ -250,6 +241,8 @@ class SpotubeAudioPlayer {
|
||||
}
|
||||
|
||||
Future<void> 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<AudioPlaybackState> _playerStateStream;
|
||||
// Playlist related
|
||||
|
||||
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);
|
||||
Future<void> openPlaylist(
|
||||
List<String> 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 {
|
||||
_playerStateStream.add(AudioPlaybackState.paused);
|
||||
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();
|
||||
}
|
||||
}),
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
53
lib/services/audio_player/loop_mode.dart
Normal file
53
lib/services/audio_player/loop_mode.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
46
lib/services/audio_player/mk_state_player.dart
Normal file
46
lib/services/audio_player/mk_state_player.dart
Normal 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);
|
||||
}
|
||||
}
|
28
lib/services/audio_player/playback_state.dart
Normal file
28
lib/services/audio_player/playback_state.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user