freezer/lib/api/player/player_helper.dart

332 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;
QueueSource? queueSource;
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 _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(
builder: () => AudioPlayerTask(initArgs),
config: AudioServiceConfig(
notificationColor: settings.primaryColor,
androidStopForegroundOnPause: true,
androidNotificationOngoing: true,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
preloadArtwork: true,
),
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
queueSource = 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 playFromAlbum(Album album, [String? trackId]) async {
await playFromTrackList(album.tracks!, trackId,
QueueSource(id: album.id, text: album.title, source: 'album'));
}
//Play mix by track
Future 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 playFromTopTracks(
List<Track> tracks, String trackId, Artist artist) async {
await playFromTrackList(
tracks,
trackId,
QueueSource(
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
}
Future 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 {
this.queueSource = 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');
// }
}