354 lines
13 KiB
Dart
354 lines
13 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/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;
|
|
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;
|
|
|
|
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 {
|
|
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 'onLoad':
|
|
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();
|
|
});
|
|
_playbackStateStreamSubscription =
|
|
audioHandler.playbackState.listen((playbackState) {
|
|
if (!_processingStateSubject.hasValue ||
|
|
_processingStateSubject.value != playbackState.processingState) {
|
|
_processingStateSubject.add(playbackState.processingState);
|
|
}
|
|
|
|
print(
|
|
'now ${playbackState.playing}, previous ${_playingSubject.valueOrNull}');
|
|
if (!_playingSubject.hasValue ||
|
|
_playingSubject.value != playbackState.playing) {
|
|
print('added!');
|
|
_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'.i18n} $trackTitle',
|
|
source: 'mix'));
|
|
}
|
|
|
|
Future<void> playSearchMixDeferred(Track track) async {
|
|
final playFuture = playFromTrackList(
|
|
[track],
|
|
null,
|
|
QueueSource(
|
|
id: track.id,
|
|
text: "${'Mix based on'.i18n} ${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'.i18n} $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');
|
|
// }
|
|
}
|