freezer/lib/api/player/player_helper.dart

305 lines
11 KiB
Dart

import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:equalizer/equalizer.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/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;
int? audioSession;
int? _prevAudioSession;
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;
/// 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 {
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: false,
androidNotificationOngoing: false,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
preloadArtwork: false,
),
);
}
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 'onLoad':
//After audio_service is loaded, load queue, set quality
await settings.updateAudioServiceQuality();
break;
case 'onRestore':
//Load queueSource from isolate
queueSource = event['queueSource'] as QueueSource;
repeatType = event['repeatMode'] as AudioServiceRepeatMode;
_queueIndex = getQueueIndex();
break;
case 'audioSession':
if (!settings.enableEqualizer) break;
//Save
_prevAudioSession = audioSession;
audioSession = event['id'];
if (audioSession == null) break;
//Open EQ
if (!equalizerOpen) {
Equalizer.open(event['id']);
equalizerOpen = true;
break;
}
//Change session id
if (_prevAudioSession != audioSession) {
if (_prevAudioSession != null) {
Equalizer.removeAudioSessionId(_prevAudioSession!);
}
Equalizer.setAudioSessionId(audioSession!);
}
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();
});
//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 onExit() async {
_customEventSubscription.cancel();
_playbackStateStreamSubscription.cancel();
_mediaItemSubscription.cancel();
}
//Replace queue, play specified track id
Future<void> _loadQueuePlay(List<MediaItem> queue, String? trackId) async {
await settings.updateAudioServiceQuality();
await audioHandler.customAction('setIndex', {
'index': trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId)
});
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))!;
playFromTrackList(
tracks,
tracks[0].id,
QueueSource(
id: trackId,
text: '${'Mix based on'.i18n} $trackTitle',
source: 'mix'));
}
//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 settings.updateAudioServiceQuality();
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<Track?> tracks, String? trackId, QueueSource queueSource) async {
final queue = await Future.wait(tracks
.map<Future<MediaItem>>((track) => track!.toMediaItem())
.toList());
await setQueueSource(queueSource);
await _loadQueuePlay(queue, 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 {
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');
// }
}