chore: create new audio player centric playback notifier with drift persistence

This commit is contained in:
Kingkor Roy Tirtho 2024-06-23 12:23:28 +06:00
parent 59041a2948
commit f79fedefd4
18 changed files with 1694 additions and 176 deletions

View File

@ -4,9 +4,9 @@ import 'dart:async';
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:spotify/spotify.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
part 'connect.freezed.dart';
part 'connect.g.dart';

View File

@ -183,7 +183,7 @@ class WebSocketEvent<T> {
if (type == WsEvent.loop) {
await callback(
WebSocketLoopEvent(
PlaybackLoopMode.fromString(data as String),
PlaylistMode.values.firstWhere((e) => e.name == data as String),
),
);
}
@ -224,12 +224,16 @@ class WebSocketEvent<T> {
}
}
class WebSocketLoopEvent extends WebSocketEvent<PlaybackLoopMode> {
WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data);
class WebSocketLoopEvent extends WebSocketEvent<PlaylistMode> {
WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data);
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
: super(
WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String));
WsEvent.loop,
PlaylistMode.values.firstWhere(
(e) => e.name == json["data"] as String,
),
);
@override
String toJson() {

View File

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:drift/drift.dart';
import 'package:encrypt/encrypt.dart';
import 'package:media_kit/media_kit.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotify/spotify.dart';
@ -25,11 +26,13 @@ part 'tables/preferences.dart';
part 'tables/scrobbler.dart';
part 'tables/skip_segment.dart';
part 'tables/source_match.dart';
part 'tables/audio_player_state.dart';
part 'typeconverters/color.dart';
part 'typeconverters/locale.dart';
part 'typeconverters/string_list.dart';
part 'typeconverters/encrypted_text.dart';
part 'typeconverters/map.dart';
@DriftDatabase(
tables: [
@ -39,6 +42,9 @@ part 'typeconverters/encrypted_text.dart';
ScrobblerTable,
SkipSegmentTable,
SourceMatchTable,
AudioPlayerStateTable,
PlaylistTable,
PlaylistMediaTable,
],
)
class AppDatabase extends _$AppDatabase {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
part of '../database.dart';
class AudioPlayerStateTable extends Table {
IntColumn get id => integer().autoIncrement()();
BoolColumn get playing => boolean()();
RealColumn get volume => real()();
TextColumn get loopMode => textEnum<PlaylistMode>()();
BoolColumn get shuffled => boolean()();
}
class PlaylistTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get audioPlayerStateId =>
integer().references(AudioPlayerStateTable, #id)();
IntColumn get index => integer()();
}
class PlaylistMediaTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get playlistId => integer().references(PlaylistTable, #id)();
TextColumn get uri => text()();
TextColumn get extras =>
text().nullable().map(const MapTypeConverter<String, dynamic>())();
TextColumn get httpHeaders =>
text().nullable().map(const MapTypeConverter<String, String>())();
}

View File

@ -0,0 +1,15 @@
part of '../database.dart';
class MapTypeConverter<K, V> extends TypeConverter<Map<K, V>, String> {
const MapTypeConverter();
@override
fromSql(String fromDb) {
return json.decode(fromDb) as Map<K, V>;
}
@override
toSql(value) {
return json.encode(value);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/collections/spotube_icons.dart';
@ -12,7 +13,6 @@ import 'package:spotube/modules/player/use_progress.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
class PlayerControls extends HookConsumerWidget {
final PaletteGenerator? palette;
@ -234,38 +234,29 @@ class PlayerControls extends HookConsumerWidget {
? null
: playlistNotifier.next,
),
StreamBuilder<PlaybackLoopMode>(
StreamBuilder<PlaylistMode>(
stream: audioPlayer.loopModeStream,
builder: (context, snapshot) {
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
final loopMode = snapshot.data ?? PlaylistMode.none;
return IconButton(
tooltip: loopMode == PlaybackLoopMode.one
tooltip: loopMode == PlaylistMode.single
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
: loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
style: loopMode == PlaylistMode.single ||
loopMode == PlaylistMode.loop
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true
? null
: () async {
audioPlayer.setLoopMode(
switch (loopMode) {
PlaybackLoopMode.all =>
PlaybackLoopMode.one,
PlaybackLoopMode.one =>
PlaybackLoopMode.none,
PlaybackLoopMode.none =>
PlaybackLoopMode.all,
},
);
await audioPlayer.setLoopMode(loopMode);
},
);
}),

View File

@ -1,4 +1,3 @@
import 'package:async/async.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@ -19,26 +18,13 @@ import 'package:spotube/services/audio_player/audio_player.dart';
final sliderValue = position.value.inSeconds;
useEffect(() {
final durationOperation =
CancelableOperation.fromFuture(audioPlayer.duration);
durationOperation.then((value) {
if (value != null) {
duration.value = value;
}
});
duration.value = audioPlayer.duration;
final durationSubscription = audioPlayer.durationStream.listen((event) {
duration.value = event;
});
final positionOperation =
CancelableOperation.fromFuture(audioPlayer.position);
positionOperation.then((value) {
if (value != null) {
position.value = value;
}
});
position.value = audioPlayer.position;
var lastPosition = position.value;
@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
});
return () {
positionOperation.cancel();
positionSubscription.cancel();
durationOperation.cancel();
durationSubscription.cancel();
};
}, []);

View File

@ -16,7 +16,7 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/utils/service_utils.dart';
class RemotePlayerQueue extends ConsumerWidget {
@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget {
: connectNotifier.next,
),
IconButton(
tooltip: loopMode == PlaybackLoopMode.one
tooltip: loopMode == PlaylistMode.single
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
: loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
style: loopMode == PlaylistMode.single ||
loopMode == PlaylistMode.loop
? activeButtonStyle
: buttonStyle,
onPressed: playlist.activeTrack == null
@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget {
: () async {
connectNotifier.setLoopMode(
switch (loopMode) {
PlaybackLoopMode.all =>
PlaybackLoopMode.one,
PlaybackLoopMode.one =>
PlaybackLoopMode.none,
PlaybackLoopMode.none =>
PlaybackLoopMode.all,
PlaylistMode.loop =>
PlaylistMode.single,
PlaylistMode.single =>
PlaylistMode.none,
PlaylistMode.none => PlaylistMode.loop,
},
);
},

View File

@ -0,0 +1,225 @@
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
Future<void> _syncSavedState() async {
final database = ref.read(databaseProvider);
var playerState =
await database.select(database.audioPlayerStateTable).getSingleOrNull();
if (playerState == null) {
await database.into(database.audioPlayerStateTable).insert(
AudioPlayerStateTableCompanion.insert(
playing: audioPlayer.isPlaying,
volume: audioPlayer.volume,
loopMode: audioPlayer.loopMode,
shuffled: audioPlayer.isShuffled,
id: const Value(0),
),
);
playerState =
await database.select(database.audioPlayerStateTable).getSingle();
} else {
await audioPlayer.setVolume(playerState.volume);
await audioPlayer.setLoopMode(playerState.loopMode);
await audioPlayer.setShuffle(playerState.shuffled);
}
var playlist =
await database.select(database.playlistTable).getSingleOrNull();
var medias = await database.select(database.playlistMediaTable).get();
if (playlist == null) {
await database.into(database.playlistTable).insert(
PlaylistTableCompanion.insert(
audioPlayerStateId: 0,
index: audioPlayer.playlist.index,
id: const Value(0),
),
);
playlist = await database.select(database.playlistTable).getSingle();
}
if (medias.isEmpty && audioPlayer.playlist.medias.isNotEmpty) {
await database.batch((batch) {
batch.insertAll(
database.playlistMediaTable,
[
for (final media in audioPlayer.playlist.medias)
PlaylistMediaTableCompanion.insert(
playlistId: playlist!.id,
uri: media.uri,
extras: Value(media.extras),
httpHeaders: Value(media.httpHeaders),
),
],
);
});
} else {
await audioPlayer.openPlaylist(
medias
.map((media) => Media(
media.uri,
extras: media.extras,
httpHeaders: media.httpHeaders,
))
.toList(),
initialIndex: playlist.index,
);
}
}
Future<void> _updatePlayerState(
AudioPlayerStateTableCompanion companion,
) async {
final database = ref.read(databaseProvider);
await (database.update(database.audioPlayerStateTable)
..where((tb) => tb.id.equals(0)))
.write(companion);
}
Future<void> _updatePlaylist(
Playlist playlist,
) async {
final database = ref.read(databaseProvider);
await database.batch((batch) {
batch.update(
database.playlistTable,
PlaylistTableCompanion(index: Value(playlist.index)),
where: (tb) => tb.id.equals(0),
);
batch.deleteAll(database.playlistMediaTable);
if (playlist.medias.isEmpty) return;
batch.insertAll(
database.playlistMediaTable,
[
for (final media in playlist.medias)
PlaylistMediaTableCompanion.insert(
playlistId: 0,
uri: media.uri,
extras: Value(media.extras),
httpHeaders: Value(media.httpHeaders),
),
],
);
});
}
@override
build() {
final subscriptions = [
audioPlayer.playingStream.listen((playing) async {
state = state.copyWith(playing: playing);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
playing: Value(playing),
),
);
}),
audioPlayer.volumeStream.listen((volume) async {
state = state.copyWith(volume: volume);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
volume: Value(volume),
),
);
}),
audioPlayer.loopModeStream.listen((loopMode) async {
state = state.copyWith(loopMode: loopMode);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
loopMode: Value(loopMode),
),
);
}),
audioPlayer.shuffledStream.listen((shuffled) async {
state = state.copyWith(shuffled: shuffled);
await _updatePlayerState(
AudioPlayerStateTableCompanion(
shuffled: Value(shuffled),
),
);
}),
audioPlayer.playlistStream.listen((playlist) async {
state = state.copyWith(playlist: playlist);
await _updatePlaylist(playlist);
}),
];
_syncSavedState();
ref.onDispose(() {
for (final subscription in subscriptions) {
subscription.cancel();
}
});
return AudioPlayerState(
loopMode: audioPlayer.loopMode,
playing: audioPlayer.isPlaying,
playlist: audioPlayer.playlist,
shuffled: audioPlayer.isShuffled,
volume: audioPlayer.volume,
);
}
// Tracks related methods
Future<void> addTrack(Track track) async {
await audioPlayer.addTrack(SpotubeMedia(track));
}
Future<void> addTracks(Iterable<Track> tracks) async {
for (final track in tracks) {
await addTrack(track);
}
}
Future<void> removeTrack(Track track) async {
final index = state.tracks.indexWhere((element) => element == track);
if (index == -1) return;
await audioPlayer.removeTrack(index);
}
Future<void> removeTracks(Iterable<Track> tracks) async {
for (final track in tracks) {
await removeTrack(track);
}
}
Future<void> load(
List<Track> track, {
required int initialIndex,
bool autoPlay = false,
}) async {
await audioPlayer.openPlaylist(
track.map((t) => SpotubeMedia(t)).toList(),
initialIndex: initialIndex,
autoPlay: autoPlay,
);
}
}
final audioPlayerProvider = NotifierProvider<AudioPlayerNotifier, AudioPlayerState>(
() => AudioPlayerNotifier(),
);

View File

@ -0,0 +1,42 @@
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/services/audio_player/audio_player.dart';
class AudioPlayerState {
final bool playing;
final double volume;
final PlaylistMode loopMode;
final bool shuffled;
final Playlist playlist;
final List<Track> tracks;
AudioPlayerState({
required this.playing,
required this.volume,
required this.loopMode,
required this.shuffled,
required this.playlist,
List<Track>? tracks,
}) : tracks = tracks ??
playlist.medias
.map((media) => SpotubeMedia.fromMedia(media).track)
.toList();
AudioPlayerState copyWith({
bool? playing,
double? volume,
PlaylistMode? loopMode,
bool? shuffled,
Playlist? playlist,
}) {
return AudioPlayerState(
playing: playing ?? this.playing,
volume: volume ?? this.volume,
loopMode: loopMode ?? this.loopMode,
shuffled: shuffled ?? this.shuffled,
playlist: playlist ?? this.playlist,
tracks: playlist == null ? tracks : null,
);
}
}

View File

@ -1,13 +1,13 @@
import 'dart:convert';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
@ -27,8 +27,8 @@ final shuffleProvider = StateProvider<bool>(
(ref) => false,
);
final loopModeProvider = StateProvider<PlaybackLoopMode>(
(ref) => PlaybackLoopMode.none,
final loopModeProvider = StateProvider<PlaylistMode>(
(ref) => PlaylistMode.none,
);
final queueProvider = StateProvider<ProxyPlaylist>(
@ -158,7 +158,7 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
emit(WebSocketShuffleEvent(value));
}
Future<void> setLoopMode(PlaybackLoopMode value) async {
Future<void> setLoopMode(PlaylistMode value) async {
emit(WebSocketLoopEvent(value));
}

View File

@ -3,11 +3,11 @@ import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
final audioPlayerLoopMode = StreamProvider<PlaybackLoopMode>((ref) {
final audioPlayerLoopMode = StreamProvider<PlaylistMode>((ref) {
return audioPlayer.loopModeStream;
});
@ -23,7 +23,7 @@ final trayMenuProvider = Provider((ref) {
final isPlaybackPlaying =
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null));
final isLoopOne =
ref.watch(audioPlayerLoopMode).asData?.value == PlaybackLoopMode.one;
ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single;
final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false;
final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false;
@ -75,7 +75,7 @@ final trayMenuProvider = Provider((ref) {
checked: isLoopOne,
onClick: (menuItem) {
audioPlayer.setLoopMode(
isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one,
isLoopOne ? PlaylistMode.none : PlaylistMode.single,
);
},
),

View File

@ -1,16 +1,16 @@
import 'dart:io';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/provider/server/server.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart';
import 'package:spotify/spotify.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/local_track.dart';
import 'package:spotube/services/audio_player/custom_player.dart';
import 'dart:async';
import 'package:media_kit/media_kit.dart' as mk;
import 'package:spotube/services/audio_player/loop_mode.dart';
import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -66,15 +66,19 @@ abstract class AudioPlayerInterface {
bool get mkSupportedPlatform => _mkSupportedPlatform;
Future<Duration?> get duration async {
Duration get duration {
return _mkPlayer.state.duration;
}
Future<Duration?> get position async {
Playlist get playlist {
return _mkPlayer.state.playlist;
}
Duration get position {
return _mkPlayer.state.position;
}
Future<Duration?> get bufferedPosition async {
Duration get bufferedPosition {
return _mkPlayer.state.buffer;
}
@ -111,8 +115,8 @@ abstract class AudioPlayerInterface {
return _mkPlayer.shuffled;
}
PlaybackLoopMode get loopMode {
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode);
PlaylistMode get loopMode {
return _mkPlayer.state.playlistMode;
}
/// Returns the current volume of the player, between 0 and 1

View File

@ -65,7 +65,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
}
String? get nextSource {
if (loopMode == PlaybackLoopMode.all &&
if (loopMode == PlaylistMode.loop &&
_mkPlayer.state.playlist.index ==
_mkPlayer.state.playlist.medias.length - 1) {
return sources.first;
@ -77,8 +77,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
}
String? get previousSource {
if (loopMode == PlaybackLoopMode.all &&
_mkPlayer.state.playlist.index == 0) {
if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) {
return sources.last;
}
@ -125,8 +124,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
await _mkPlayer.setShuffle(shuffle);
}
Future<void> setLoopMode(PlaybackLoopMode loop) async {
await _mkPlayer.setPlaylistMode(loop.toPlaylistMode());
Future<void> setLoopMode(PlaylistMode loop) async {
await _mkPlayer.setPlaylistMode(loop);
}
Future<void> setAudioNormalization(bool normalize) async {

View File

@ -71,12 +71,12 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
// }
}
Stream<PlaybackLoopMode> get loopModeStream {
Stream<PlaylistMode> get loopModeStream {
// if (mkSupportedPlatform) {
return _mkPlayer.stream.playlistMode.map(PlaybackLoopMode.fromPlaylistMode);
return _mkPlayer.stream.playlistMode;
// } else {
// return _justAudio!.loopModeStream
// .map(PlaybackLoopMode.fromLoopMode)
// .map(PlaylistMode.fromLoopMode)
// ;
// }
}

View File

@ -1,90 +0,0 @@
import 'package:audio_service/audio_service.dart';
import 'package:media_kit/media_kit.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;
}
}
static PlaybackLoopMode fromAudioServiceRepeatMode(
AudioServiceRepeatMode mode) {
switch (mode) {
case AudioServiceRepeatMode.all:
case AudioServiceRepeatMode.group:
return PlaybackLoopMode.all;
case AudioServiceRepeatMode.one:
return PlaybackLoopMode.one;
case AudioServiceRepeatMode.none:
return PlaybackLoopMode.none;
}
}
AudioServiceRepeatMode toAudioServiceRepeatMode() {
switch (this) {
case PlaybackLoopMode.all:
return AudioServiceRepeatMode.all;
case PlaybackLoopMode.one:
return AudioServiceRepeatMode.one;
case PlaybackLoopMode.none:
return AudioServiceRepeatMode.none;
}
}
static PlaybackLoopMode fromString(String? value) {
switch (value) {
case 'all':
return PlaybackLoopMode.all;
case 'one':
return PlaybackLoopMode.one;
case 'none':
return PlaybackLoopMode.none;
default:
return PlaybackLoopMode.none;
}
}
}

View File

@ -5,7 +5,7 @@ import 'package:audio_session/audio_session.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
import 'package:media_kit/media_kit.dart' hide Track;
class MobileAudioService extends BaseAudioHandler {
AudioSession? session;
@ -91,9 +91,13 @@ class MobileAudioService extends BaseAudioHandler {
@override
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
super.setRepeatMode(repeatMode);
audioPlayer.setLoopMode(
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
);
audioPlayer.setLoopMode(switch (repeatMode) {
AudioServiceRepeatMode.all ||
AudioServiceRepeatMode.group =>
PlaylistMode.loop,
AudioServiceRepeatMode.one => PlaylistMode.single,
_ => PlaylistMode.none,
});
}
@override
@ -120,7 +124,6 @@ class MobileAudioService extends BaseAudioHandler {
}
Future<PlaybackState> _transformEvent() async {
final position = (await audioPlayer.position) ?? Duration.zero;
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
@ -133,12 +136,16 @@ class MobileAudioService extends BaseAudioHandler {
},
androidCompactActionIndices: const [0, 1, 2],
playing: audioPlayer.isPlaying,
updatePosition: position,
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
updatePosition: audioPlayer.position,
bufferedPosition: audioPlayer.bufferedPosition,
shuffleMode: audioPlayer.isShuffled == true
? AudioServiceShuffleMode.all
: AudioServiceShuffleMode.none,
repeatMode: (audioPlayer.loopMode).toAudioServiceRepeatMode(),
repeatMode: switch (audioPlayer.loopMode) {
PlaylistMode.loop => AudioServiceRepeatMode.all,
PlaylistMode.single => AudioServiceRepeatMode.one,
_ => AudioServiceRepeatMode.none,
},
processingState: playlist.isFetching == true
? AudioProcessingState.loading
: AudioProcessingState.ready,