Pato05
87c9733f51
fix audio service stop on android getTrack backend improvements get new track token when expired move shuffle button into LibraryPlaylists as FAB move favoriteButton next to track title move lyrics button on top of album art search: fix chips, and remove checkbox when selected
344 lines
12 KiB
Dart
344 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
import 'package:freezer/api/cache.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/main.dart';
|
|
import 'package:freezer/settings.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:rxdart/rxdart.dart';
|
|
|
|
class PlayerHelper {
|
|
late StreamSubscription _customEventSubscription;
|
|
late StreamSubscription _mediaItemSubscription;
|
|
late StreamSubscription _playbackStateStreamSubscription;
|
|
AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none;
|
|
bool equalizerOpen = false;
|
|
bool _shuffleEnabled = false;
|
|
int _queueIndex = 0;
|
|
bool _started = false;
|
|
|
|
/// Whether this system supports the [Connectivity] plugin or not
|
|
bool _isConnectivityPluginAvailable = true;
|
|
bool get isConnectivityPluginAvailable => _isConnectivityPluginAvailable;
|
|
|
|
//Visualizer
|
|
// StreamController _visualizerController = StreamController.broadcast();
|
|
// Stream get visualizerStream => _visualizerController.stream;
|
|
|
|
final _queueSourceSubject = BehaviorSubject<QueueSource>();
|
|
ValueStream<QueueSource> get queueSource => _queueSourceSubject.stream;
|
|
|
|
final _streamInfoSubject = BehaviorSubject<StreamQualityInfo>();
|
|
ValueStream<StreamQualityInfo> get streamInfo => _streamInfoSubject.stream;
|
|
|
|
final _bufferPositionSubject = BehaviorSubject<Duration>();
|
|
ValueStream<Duration> get bufferPosition => _bufferPositionSubject.stream;
|
|
|
|
final _playingSubject = BehaviorSubject<bool>();
|
|
ValueStream<bool> get playing => _playingSubject.stream;
|
|
|
|
final _processingStateSubject = BehaviorSubject<AudioProcessingState>();
|
|
ValueStream<AudioProcessingState> get processingState =>
|
|
_processingStateSubject.stream;
|
|
|
|
/// Find queue index by id
|
|
///
|
|
/// The function gets more expensive the longer the queue is and the further the element is from the beginning.
|
|
int getQueueIndexFromId() => audioHandler.mediaItem.value == null
|
|
? -1
|
|
: audioHandler.queue.value
|
|
.indexWhere((mi) => mi.id == audioHandler.mediaItem.value!.id);
|
|
|
|
int getQueueIndex() =>
|
|
audioHandler.playbackState.value.queueIndex ?? getQueueIndexFromId();
|
|
|
|
int get queueIndex => _queueIndex;
|
|
|
|
Future<void> initAudioHandler() async {
|
|
if (failsafe) {
|
|
Fluttertoast.showToast(msg: 'what the fuck?');
|
|
return;
|
|
}
|
|
failsafe = true;
|
|
|
|
final initArgs = AudioPlayerTaskInitArguments.from(
|
|
settings: settings, deezerAPI: deezerAPI);
|
|
// initialize our audiohandler instance
|
|
audioHandler = await AudioService.init<AudioPlayerTask>(
|
|
builder: () => AudioPlayerTask(initArgs),
|
|
config: AudioServiceConfig(
|
|
notificationColor: settings.primaryColor,
|
|
androidStopForegroundOnPause: true,
|
|
androidNotificationOngoing: true,
|
|
androidNotificationClickStartsActivity: true,
|
|
androidNotificationChannelDescription: 'Freezer',
|
|
androidNotificationChannelName: 'Freezer',
|
|
androidNotificationIcon: 'drawable/ic_logo',
|
|
androidNotificationChannelId: 'f.f.freezer.audio',
|
|
preloadArtwork: true,
|
|
androidBrowsableRootExtras: <String, dynamic>{
|
|
"android.media.browse.SEARCH_SUPPORTED":
|
|
true, // support showing search button on Android Auto as well as alternative search results on the player screen after voice search
|
|
},
|
|
),
|
|
cacheManager: cacheManager,
|
|
);
|
|
}
|
|
|
|
Future<void> start() async {
|
|
if (_started) return;
|
|
_started = true;
|
|
//Subscribe to custom events
|
|
_customEventSubscription = audioHandler.customEvent.listen((event) async {
|
|
if (event is! Map) return;
|
|
Logger('PlayerHelper').fine("event received: ${event['action']}");
|
|
switch (event['action']) {
|
|
case 'onRestore':
|
|
//Load queueSource from isolate
|
|
_queueSourceSubject.add(event['queueSource'] as QueueSource);
|
|
repeatType = event['repeatMode'] as AudioServiceRepeatMode;
|
|
_queueIndex = getQueueIndex();
|
|
break;
|
|
//Visualizer data
|
|
// case 'visualizer':
|
|
// _visualizerController.add(event['data']);
|
|
// break;
|
|
case 'streamInfo':
|
|
Logger('PlayerHelper').fine("streamInfo received");
|
|
_streamInfoSubject.add(event['data'] as StreamQualityInfo);
|
|
break;
|
|
case 'bufferPosition':
|
|
_bufferPositionSubject.add(event['data'] as Duration);
|
|
break;
|
|
case 'connectivityPlugin':
|
|
_isConnectivityPluginAvailable = event['available'] as bool;
|
|
break;
|
|
}
|
|
});
|
|
_mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async {
|
|
if (mediaItem == null) return;
|
|
_queueIndex = getQueueIndex();
|
|
//Load more flow if last song (not using .last since it iterates through previous elements first)
|
|
|
|
//Save queue
|
|
await audioHandler.customAction('saveQueue', {});
|
|
//Add to history
|
|
if (cache.history.isNotEmpty && cache.history.last.id == mediaItem.id) {
|
|
return;
|
|
}
|
|
cache.history.add(Track.fromMediaItem(mediaItem));
|
|
cache.save();
|
|
});
|
|
_playbackStateStreamSubscription =
|
|
audioHandler.playbackState.listen((playbackState) {
|
|
if (!_processingStateSubject.hasValue ||
|
|
_processingStateSubject.value != playbackState.processingState) {
|
|
_processingStateSubject.add(playbackState.processingState);
|
|
}
|
|
|
|
if (!_playingSubject.hasValue ||
|
|
_playingSubject.value != playbackState.playing) {
|
|
_playingSubject.add(playbackState.playing);
|
|
}
|
|
});
|
|
|
|
//Start audio_service
|
|
// await startService(); it is already ready, there is no need to start it
|
|
}
|
|
|
|
Future<void> authorizeLastFM() async {
|
|
if (settings.lastFMUsername == null || settings.lastFMPassword == null) {
|
|
return;
|
|
}
|
|
await audioHandler.customAction('authorizeLastFM', {
|
|
'username': settings.lastFMUsername,
|
|
'password': settings.lastFMPassword
|
|
});
|
|
}
|
|
|
|
Future<bool> toggleShuffle() async {
|
|
_shuffleEnabled = !_shuffleEnabled;
|
|
await audioHandler.setShuffleMode(_shuffleEnabled
|
|
? AudioServiceShuffleMode.all
|
|
: AudioServiceShuffleMode.none);
|
|
return _shuffleEnabled;
|
|
}
|
|
|
|
bool get shuffleEnabled => _shuffleEnabled;
|
|
|
|
//Repeat toggle
|
|
Future changeRepeat() async {
|
|
//Change to next repeat type
|
|
repeatType = repeatType == AudioServiceRepeatMode.all
|
|
? AudioServiceRepeatMode.none
|
|
: repeatType == AudioServiceRepeatMode.none
|
|
? AudioServiceRepeatMode.one
|
|
: AudioServiceRepeatMode.all;
|
|
//Set repeat type
|
|
await audioHandler.setRepeatMode(repeatType);
|
|
}
|
|
|
|
//Executed before exit
|
|
Future stop() async {
|
|
_customEventSubscription.cancel();
|
|
_playbackStateStreamSubscription.cancel();
|
|
_mediaItemSubscription.cancel();
|
|
_started = false;
|
|
}
|
|
|
|
//Replace queue, play specified track id
|
|
Future<void> _loadQueuePlay(List<MediaItem> queue, int? index) async {
|
|
if (index != null) {
|
|
await audioHandler.customAction('setIndex', {'index': index});
|
|
}
|
|
await audioHandler.updateQueue(queue);
|
|
// if (queue[0].id != trackId)
|
|
// await AudioService.skipToQueueItem(trackId);
|
|
if (!audioHandler.playbackState.value.playing) audioHandler.play();
|
|
}
|
|
|
|
//Play track from album
|
|
Future<void> playFromAlbum(Album album, [String? trackId]) async {
|
|
if (album.tracks!.length == 1) {
|
|
// play mix based on track
|
|
await playSearchMixDeferred(album.tracks![0]);
|
|
return;
|
|
}
|
|
await playFromTrackList(album.tracks!, trackId,
|
|
QueueSource(id: album.id, text: album.title, source: 'album'));
|
|
}
|
|
|
|
//Play mix by track
|
|
Future<void> playMix(String trackId, String trackTitle) async {
|
|
List<Track> tracks = await deezerAPI.playMix(trackId);
|
|
await playFromTrackList(
|
|
tracks,
|
|
tracks[0].id,
|
|
QueueSource(
|
|
id: trackId,
|
|
text: 'Mix based on %s'.i18n.fill([trackTitle]),
|
|
source: 'mix'));
|
|
}
|
|
|
|
Future<void> playSearchMixDeferred(Track track) async {
|
|
final playFuture = playFromTrackList(
|
|
[track],
|
|
null,
|
|
QueueSource(
|
|
id: track.id,
|
|
text: 'Mix based on %s'.i18n.fill([track.title!]),
|
|
source: 'searchMix'));
|
|
List<Track> tracks = await deezerAPI.getSearchTrackMix(track.id, false);
|
|
// discard first track (if it is the searched track)
|
|
if (tracks[0].id == track.id) tracks.removeAt(0);
|
|
await playFuture; // avoid race conditions
|
|
|
|
// update queue with mix
|
|
await audioHandler.addQueueItems(
|
|
tracks.map((e) => e.toMediaItem()).toList(growable: false));
|
|
}
|
|
|
|
Future<void> playSearchMix(String trackId, String trackTitle) async {
|
|
List<Track> tracks = await deezerAPI.getSearchTrackMix(trackId, true);
|
|
await playFromTrackList(
|
|
tracks,
|
|
null, // we can avoid passing it, as the index is 0
|
|
QueueSource(
|
|
id: trackId,
|
|
text: 'Mix based on %s'.i18n.fill([trackTitle]),
|
|
source: 'searchMix'));
|
|
}
|
|
|
|
//Play from artist top tracks
|
|
Future<void> playFromTopTracks(
|
|
List<Track> tracks, String? trackId, Artist artist) async {
|
|
await playFromTrackList(
|
|
tracks,
|
|
trackId,
|
|
QueueSource(
|
|
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
|
|
}
|
|
|
|
Future<void> playFromPlaylist(Playlist playlist, [String? trackId]) async {
|
|
await playFromTrackList(playlist.tracks!, trackId,
|
|
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
|
}
|
|
|
|
//Play episode from show, load whole show as queue
|
|
Future<void> playShowEpisode(Show show, List<ShowEpisode> episodes,
|
|
{int index = 0}) async {
|
|
QueueSource queueSource =
|
|
QueueSource(id: show.id, text: show.name, source: 'show');
|
|
//Generate media items
|
|
List<MediaItem> queue =
|
|
episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
|
|
|
|
//Load and play
|
|
// await startService(); // audioservice is ready
|
|
await setQueueSource(queueSource);
|
|
await audioHandler.customAction('setIndex', {'index': index});
|
|
await audioHandler.updateQueue(queue);
|
|
if (!audioHandler.playbackState.value.playing) audioHandler.play();
|
|
}
|
|
|
|
//Load tracks as queue, play track id, set queue source
|
|
Future<void> playFromTrackList(
|
|
List<Track> tracks, String? trackId, QueueSource queueSource) async {
|
|
final queue =
|
|
tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
|
|
await setQueueSource(queueSource);
|
|
await _loadQueuePlay(
|
|
queue, trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId));
|
|
}
|
|
|
|
//Load smart track list as queue, start from beginning
|
|
Future<void> playFromSmartTrackList(SmartTrackList stl) async {
|
|
//Load from API if no tracks
|
|
if (stl.tracks == null || stl.tracks!.isEmpty) {
|
|
if (settings.offlineMode) {
|
|
Fluttertoast.showToast(
|
|
msg: "Offline mode, can't play flow or smart track lists.".i18n,
|
|
gravity: ToastGravity.BOTTOM,
|
|
toastLength: Toast.LENGTH_SHORT);
|
|
return;
|
|
}
|
|
|
|
//Flow songs cannot be accessed by smart track list call
|
|
if (stl.id! == 'flow') {
|
|
stl.tracks = await deezerAPI.flow(stl.flowConfig);
|
|
} else {
|
|
stl = await deezerAPI.smartTrackList(stl.id);
|
|
}
|
|
}
|
|
QueueSource queueSource = QueueSource(
|
|
id: stl.flowConfig ?? stl.id,
|
|
source: (stl.id == 'flow') ? 'flow' : 'smarttracklist',
|
|
text: stl.title ??
|
|
((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
|
|
await playFromTrackList(stl.tracks!, stl.tracks![0].id, queueSource);
|
|
}
|
|
|
|
Future setQueueSource(QueueSource queueSource) async {
|
|
_queueSourceSubject.add(queueSource);
|
|
await audioHandler.customAction('queueSource', queueSource.toJson());
|
|
}
|
|
|
|
//Reorder tracks in queue
|
|
Future reorder(int oldIndex, int newIndex) => audioHandler
|
|
.customAction('reorder', {'oldIndex': oldIndex, 'newIndex': newIndex});
|
|
|
|
//Start visualizer
|
|
// Future startVisualizer() async {
|
|
// await audioHandler.customAction('startVisualizer');
|
|
// }
|
|
|
|
//Stop visualizer
|
|
// Future stopVisualizer() async {
|
|
// await audioHandler.customAction('stopVisualizer');
|
|
// }
|
|
}
|