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(); ValueStream get queueSource => _queueSourceSubject.stream; final _streamInfoSubject = BehaviorSubject(); ValueStream get streamInfo => _streamInfoSubject.stream; final _bufferPositionSubject = BehaviorSubject(); ValueStream get bufferPosition => _bufferPositionSubject.stream; final _playingSubject = BehaviorSubject(); ValueStream get playing => _playingSubject.stream; final _processingStateSubject = BehaviorSubject(); ValueStream 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 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', androidNotificationChannelId: 'f.f.freezer.audio', preloadArtwork: true, androidBrowsableRootExtras: { "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 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 authorizeLastFM() async { if (settings.lastFMUsername == null || settings.lastFMPassword == null) { return; } await audioHandler.customAction('authorizeLastFM', { 'username': settings.lastFMUsername, 'password': settings.lastFMPassword }); } Future 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 _loadQueuePlay(List 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 { 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 playMix(String trackId, String trackTitle) async { List 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 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 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 playSearchMix(String trackId, String trackTitle) async { List 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 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 playShowEpisode(Show show, List episodes, {int index = 0}) async { QueueSource queueSource = QueueSource(id: show.id, text: show.name, source: 'show'); //Generate media items List queue = episodes.map((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 playFromTrackList( List tracks, String? trackId, QueueSource queueSource) async { final queue = tracks.map((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 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'); // } }