freezer/lib/api/player.dart
2021-08-30 14:51:51 +02:00

963 lines
30 KiB
Dart

import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.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/ui/android_auto.dart';
import 'package:just_audio/just_audio.dart';
import 'package:connectivity/connectivity.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:scrobblenaut/scrobblenaut.dart';
import 'definitions.dart';
import '../settings.dart';
import 'dart:io';
import 'dart:async';
import 'dart:convert';
import 'dart:math';
PlayerHelper playerHelper = PlayerHelper();
class PlayerHelper {
StreamSubscription _customEventSubscription;
StreamSubscription _mediaItemSubscription;
StreamSubscription _playbackStateStreamSubscription;
QueueSource queueSource;
LoopMode repeatType = LoopMode.off;
Timer _timer;
int audioSession;
int _prevAudioSession;
bool equalizerOpen = false;
//Visualizer
StreamController _visualizerController = StreamController.broadcast();
Stream get visualizerStream => _visualizerController.stream;
//Find queue index by id
int get queueIndex => AudioService.queue == null
? 0
: AudioService.queue
.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id);
Future start() async {
//Subscribe to custom events
_customEventSubscription =
AudioService.customEventStream.listen((event) async {
if (!(event is Map)) return;
switch (event['action']) {
case 'onLoad':
//After audio_service is loaded, load queue, set quality
await settings.updateAudioServiceQuality();
await AudioService.customAction('load');
await authorizeLastFM();
break;
case 'onRestore':
//Load queueSource from isolate
this.queueSource = QueueSource.fromJson(event['queueSource']);
repeatType = LoopMode.values[event['loopMode']];
break;
case 'queueEnd':
//If last song is played, load more queue
this.queueSource = QueueSource.fromJson(event['queueSource']);
// onQueueEnd();
break;
case 'screenAndroidAuto':
AndroidAuto androidAuto = AndroidAuto();
List<MediaItem> data = await androidAuto.getScreen(event['id']);
await AudioService.customAction(
'screenAndroidAuto', jsonEncode(data));
break;
case 'tracksAndroidAuto':
AndroidAuto androidAuto = AndroidAuto();
await androidAuto.playItem(event['id']);
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;
}
});
_mediaItemSubscription =
AudioService.currentMediaItemStream.listen((event) {
if (event == null) return;
//Load more flow if index-1 song
if (queueIndex == AudioService.queue.length - 1) onQueueEnd();
//Save queue
AudioService.customAction('saveQueue');
//Add to history
if (cache.history == null) cache.history = [];
if (cache.history.length > 0 && cache.history.last.id == event.id) return;
cache.history.add(Track.fromMediaItem(event));
cache.save();
});
//Logging listen timer
_timer = Timer.periodic(Duration(seconds: 2), (timer) async {
if (AudioService.currentMediaItem == null ||
!AudioService.playbackState.playing) return;
if (AudioService.playbackState.currentPosition.inSeconds >
(AudioService.currentMediaItem.duration.inSeconds * 0.75)) {
if (cache.loggedTrackId == AudioService.currentMediaItem.id) return;
cache.loggedTrackId = AudioService.currentMediaItem.id;
await cache.save();
//Log to Deezer
if (settings.logListen) {
deezerAPI.logListen(AudioService.currentMediaItem.id);
}
}
});
//Start audio_service
await startService();
}
Future startService() async {
if (AudioService.running && AudioService.connected) return;
if (!AudioService.connected) await AudioService.connect();
if (!AudioService.running)
await AudioService.start(
backgroundTaskEntrypoint: backgroundTaskEntrypoint,
androidEnableQueue: true,
androidStopForegroundOnPause: false,
androidNotificationOngoing: false,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
params: {'ignoreInterruptions': settings.ignoreInterruptions});
}
Future authorizeLastFM() async {
if (settings.lastFMUsername == null || settings.lastFMPassword == null)
return;
await AudioService.customAction(
"authorizeLastFM", [settings.lastFMUsername, settings.lastFMPassword]);
}
Future toggleShuffle() async {
await AudioService.customAction('shuffle');
}
//Repeat toggle
Future changeRepeat() async {
//Change to next repeat type
switch (repeatType) {
case LoopMode.one:
repeatType = LoopMode.off;
break;
case LoopMode.all:
repeatType = LoopMode.one;
break;
default:
repeatType = LoopMode.all;
break;
}
//Set repeat type
await AudioService.customAction(
"repeatType", LoopMode.values.indexOf(repeatType));
}
//Executed before exit
Future onExit() async {
_customEventSubscription.cancel();
_playbackStateStreamSubscription.cancel();
_mediaItemSubscription.cancel();
}
//Replace queue, play specified track id
Future _loadQueuePlay(List<MediaItem> queue, String trackId) async {
await startService();
await settings.updateAudioServiceQuality();
await AudioService.customAction(
'setIndex', queue.indexWhere((m) => m.id == trackId));
await AudioService.updateQueue(queue);
// if (queue[0].id != trackId)
// await AudioService.skipToQueueItem(trackId);
if (!AudioService.playbackState.playing) AudioService.play();
}
//Called when queue ends to load more tracks
Future onQueueEnd() async {
//Flow
if (queueSource == null) return;
List<Track> tracks = [];
switch (queueSource.source) {
case 'flow':
tracks = await deezerAPI.flow();
break;
//SmartRadio/Artist radio
case 'smartradio':
tracks = await deezerAPI.smartRadio(queueSource.id);
break;
//Library shuffle
case 'libraryshuffle':
tracks =
await deezerAPI.libraryShuffle(start: AudioService.queue.length);
break;
case 'mix':
tracks = await deezerAPI.playMix(queueSource.id);
// Deduplicate tracks with the same id
List<String> queueIds = AudioService.queue.map((e) => e.id).toList();
tracks.removeWhere((track) => queueIds.contains(track.id));
break;
default:
// print(queueSource.toJson());
break;
}
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
await AudioService.addQueueItems(mi);
// AudioService.skipToNext();
}
//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 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();
await settings.updateAudioServiceQuality();
await setQueueSource(queueSource);
await AudioService.customAction('setIndex', index);
await AudioService.updateQueue(queue);
if (!AudioService.playbackState.playing) AudioService.play();
}
//Load tracks as queue, play track id, set queue source
Future playFromTrackList(
List<Track> tracks, String trackId, QueueSource queueSource) async {
await startService();
List<MediaItem> queue =
tracks.map<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.length == 0) {
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();
} else {
stl = await deezerAPI.smartTrackList(stl.id);
}
}
QueueSource queueSource = QueueSource(
id: 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 {
await startService();
this.queueSource = queueSource;
await AudioService.customAction('queueSource', queueSource.toJson());
}
//Reorder tracks in queue
Future reorder(int oldIndex, int newIndex) async {
await AudioService.customAction('reorder', [oldIndex, newIndex]);
}
//Start visualizer
// Future startVisualizer() async {
// await AudioService.customAction('startVisualizer');
// }
//Stop visualizer
Future stopVisualizer() async {
await AudioService.customAction('stopVisualizer');
}
}
void backgroundTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player;
//Queue
List<MediaItem> _queue = <MediaItem>[];
List<MediaItem> _originalQueue;
bool _shuffle = false;
int _queueIndex = 0;
ConcatenatingAudioSource _audioSource;
AudioProcessingState _skipState;
Seeker _seeker;
//Stream subscriptions
StreamSubscription _eventSub;
StreamSubscription _audioSessionSub;
StreamSubscription _visualizerSubscription;
//Loaded from file/frontend
int mobileQuality;
int wifiQuality;
QueueSource queueSource;
Duration _lastPosition;
LoopMode _loopMode = LoopMode.off;
Completer _androidAutoCallback;
Scrobblenaut _scrobblenaut;
bool _scrobblenautReady = false;
// Last logged track id
String _loggedTrackId;
MediaItem get mediaItem => _queue[_queueIndex];
@override
Future onStart(Map<String, dynamic> params) async {
final session = await AudioSession.instance;
session.configure(AudioSessionConfiguration.music());
if (params['ignoreInterruptions'] == true) {
_player = AudioPlayer(handleInterruptions: false);
session.interruptionEventStream.listen((_) {});
session.becomingNoisyEventStream.listen((_) {});
} else
_player = AudioPlayer();
//Update track index
_player.currentIndexStream.listen((index) {
if (index != null) {
_queueIndex = index;
AudioServiceBackground.setMediaItem(mediaItem);
}
});
//Update state on all clients on change
_eventSub = _player.playbackEventStream.listen((event) {
//Quality string
if (_queueIndex != -1 && _queueIndex < _queue.length) {
Map extras = mediaItem.extras;
extras['qualityString'] = '';
_queue[_queueIndex] = mediaItem.copyWith(extras: extras);
}
//Update
_broadcastState();
});
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
//Player ended, get more songs
if (_queueIndex == _queue.length - 1)
AudioServiceBackground.sendCustomEvent({
'action': 'queueEnd',
'queueSource': (queueSource ?? QueueSource()).toJson()
});
break;
case ProcessingState.ready:
//Ready to play
_skipState = null;
break;
default:
break;
}
});
//Audio session
_audioSessionSub = _player.androidAudioSessionIdStream.listen((event) {
AudioServiceBackground.sendCustomEvent(
{"action": 'audioSession', "id": event});
});
//Load queue
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.sendCustomEvent({'action': 'onLoad'});
}
@override
Future onSkipToQueueItem(String mediaId) async {
_lastPosition = null;
//Calculate new index
final newIndex = _queue.indexWhere((i) => i.id == mediaId);
if (newIndex == -1) return;
//Update buffering state
_skipState = newIndex > _queueIndex
? AudioProcessingState.skippingToNext
: AudioProcessingState.skippingToPrevious;
//Skip in player
await _player.seek(Duration.zero, index: newIndex);
_queueIndex = newIndex;
_skipState = null;
onPlay();
}
@override
Future onPlay() async {
_player.play();
//Restore position on play
if (_lastPosition != null) {
onSeekTo(_lastPosition);
_lastPosition = null;
}
//LastFM
if (_scrobblenautReady && mediaItem.id != _loggedTrackId) {
_loggedTrackId = mediaItem.id;
await _scrobblenaut.track.scrobble(
track: mediaItem.title,
artist: mediaItem.artist,
album: mediaItem.album,
);
}
}
@override
Future onPause() => _player.pause();
@override
Future onSeekTo(Duration pos) => _player.seek(pos);
@override
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
@override
Future<void> onRewind() => _seekRelative(-rewindInterval);
@override
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
@override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
//Remove item from queue
@override
Future<void> onRemoveQueueItem(MediaItem mediaItem) async {
int index = _queue.indexWhere((m) => m.id == mediaItem.id);
_queue.removeAt(index);
if (index <= _queueIndex) {
_queueIndex--;
}
_audioSource.removeAt(index);
AudioServiceBackground.setQueue(_queue);
}
@override
Future<void> onSkipToNext() async {
_lastPosition = null;
if (_queueIndex == _queue.length - 1) return;
//Update buffering state
_skipState = AudioProcessingState.skippingToNext;
_queueIndex++;
await _player.seekToNext();
_skipState = null;
await _broadcastState();
}
@override
Future<void> onSkipToPrevious() async {
if (_queueIndex == 0) return;
//Update buffering state
_skipState = AudioProcessingState.skippingToPrevious;
//Normal skip to previous
_queueIndex--;
await _player.seekToPrevious();
_skipState = null;
}
@override
Future<List<MediaItem>> onLoadChildren(String parentMediaId) async {
AudioServiceBackground.sendCustomEvent(
{'action': 'screenAndroidAuto', 'id': parentMediaId});
//Wait for data from main thread
_androidAutoCallback = Completer();
List<MediaItem> data =
(await _androidAutoCallback.future) as List<MediaItem>;
_androidAutoCallback = null;
return data;
}
//While seeking, jump 10s every 1s
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
if (begin) {
_seeker = Seeker(_player, Duration(seconds: 10 * direction),
Duration(seconds: 1), mediaItem)
..start();
}
}
//Relative seek
Future _seekRelative(Duration offset) async {
Duration newPos = _player.position + offset;
//Out of bounds check
if (newPos < Duration.zero) newPos = Duration.zero;
if (newPos > mediaItem.duration) newPos = mediaItem.duration;
await _player.seek(newPos);
}
//Update state on all clients
Future _broadcastState() async {
await AudioServiceBackground.setState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
//Stop
MediaControl(
androidIcon: 'drawable/ic_action_stop',
label: 'stop',
action: MediaAction.stop),
],
systemActions: [
MediaAction.seekTo,
MediaAction.seekForward,
MediaAction.seekBackward,
MediaAction.stop
],
processingState: _getProcessingState(),
playing: _player.playing,
position: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed);
}
//just_audio state -> audio_service state. If skipping, use _skipState
AudioProcessingState _getProcessingState() {
if (_skipState != null) return _skipState;
//SRC: audio_service example
switch (_player.processingState) {
case ProcessingState.idle:
return AudioProcessingState.stopped;
case ProcessingState.loading:
return AudioProcessingState.connecting;
case ProcessingState.buffering:
return AudioProcessingState.buffering;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
}
//Replace current queue
@override
Future onUpdateQueue(List<MediaItem> q) async {
_lastPosition = null;
//just_audio
_shuffle = false;
_originalQueue = null;
_player.stop();
if (_audioSource != null) _audioSource.clear();
//Filter duplicate IDs
List<MediaItem> queue = [];
for (MediaItem mi in q) {
if (queue.indexWhere((m) => mi.id == m.id) == -1) queue.add(mi);
}
this._queue = queue;
AudioServiceBackground.setQueue(queue);
//Load
await _loadQueue();
//await _player.seek(Duration.zero, index: 0);
}
//Load queue to just_audio
Future _loadQueue() async {
//Don't reset queue index by starting player
int qi = _queueIndex;
List<AudioSource> sources = [];
for (int i = 0; i < _queue.length; i++) {
AudioSource s = await _mediaItemToAudioSource(_queue[i]);
if (s != null) sources.add(s);
}
_audioSource = ConcatenatingAudioSource(children: sources);
//Load in just_audio
try {
await _player.setAudioSource(_audioSource,
initialIndex: qi, initialPosition: Duration.zero);
} catch (e) {
//Error loading tracks
}
_queueIndex = qi;
AudioServiceBackground.setMediaItem(mediaItem);
}
Future<AudioSource> _mediaItemToAudioSource(MediaItem mi) async {
String url = await _getTrackUrl(mi);
if (url == null) return null;
if (url.startsWith('http')) return ProgressiveAudioSource(Uri.parse(url));
return AudioSource.uri(Uri.parse(url), tag: mi.id);
}
Future _getTrackUrl(MediaItem mediaItem, {int quality}) async {
//Check if offline
String _offlinePath =
p.join((await getExternalStorageDirectory()).path, 'offline/');
File f = File(p.join(_offlinePath, mediaItem.id));
if (await f.exists()) {
//return f.path;
//Stream server URL
return 'http://localhost:36958/?id=${mediaItem.id}';
}
//Show episode direct link
if (mediaItem.extras['showUrl'] != null) return mediaItem.extras['showUrl'];
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
//This just returns fake url that contains metadata
List playbackDetails = jsonDecode(mediaItem.extras['playbackDetails']);
//Quality
ConnectivityResult conn = await Connectivity().checkConnectivity();
quality = mobileQuality;
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
if ((playbackDetails ?? []).length < 2) return null;
//String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
String url =
'http://localhost:36958/?q=$quality&mv=${playbackDetails[1]}&md5origin=${playbackDetails[0]}&id=${mediaItem.id}';
return url;
}
//Custom actions
@override
Future onCustomAction(String name, dynamic args) async {
switch (name) {
case 'updateQuality':
//Pass wifi & mobile quality by custom action
//Isolate can't access globals
this.wifiQuality = args['wifiQuality'];
this.mobileQuality = args['mobileQuality'];
break;
//Update queue source
case 'queueSource':
this.queueSource =
QueueSource.fromJson(Map<String, dynamic>.from(args));
break;
//Looping
case 'repeatType':
_loopMode = LoopMode.values[args];
_player.setLoopMode(_loopMode);
break;
//Save queue
case 'saveQueue':
await this._saveQueue();
break;
//Load queue after some initialization in frontend
case 'load':
await this._loadQueueFile();
break;
case 'shuffle':
String originalId = mediaItem.id;
if (!_shuffle) {
_shuffle = true;
_originalQueue = List.from(_queue);
_queue.shuffle();
} else {
_shuffle = false;
_queue = _originalQueue;
_originalQueue = null;
}
//Broken
// _queueIndex = _queue.indexWhere((mi) => mi.id == originalId);
_queueIndex = 0;
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await _player.stop();
await _loadQueue();
await _player.play();
break;
//Android audio callback
case 'screenAndroidAuto':
if (_androidAutoCallback != null)
_androidAutoCallback.complete(jsonDecode(args)
.map<MediaItem>((m) => MediaItem.fromJson(m))
.toList());
break;
//Reorder tracks, args = [old, new]
case 'reorder':
await _audioSource.move(args[0], args[1]);
//Switch in queue
_queue.reorder(args[0], args[1]);
//Update UI
AudioServiceBackground.setQueue(_queue);
_broadcastState();
break;
//Set index without affecting playback for loading
case 'setIndex':
this._queueIndex = args;
break;
//Start visualizer
// case 'startVisualizer':
// if (_visualizerSubscription != null) break;
// _player.startVisualizer(
// enableWaveform: false,
// enableFft: true,
// captureRate: 15000,
// captureSize: 128);
// _visualizerSubscription = _player.visualizerFftStream.listen((event) {
// //Calculate actual values
// List<double> out = [];
// for (int i = 0; i < event.length / 2; i++) {
// int rfk = event[i * 2].toSigned(8);
// int ifk = event[i * 2 + 1].toSigned(8);
// out.add(log(hypot(rfk, ifk) + 1) / 5.2);
// }
// AudioServiceBackground.sendCustomEvent(
// {"action": "visualizer", "data": out});
// });
// break;
// //Stop visualizer
// case 'stopVisualizer':
// if (_visualizerSubscription != null) {
// _visualizerSubscription.cancel();
// _visualizerSubscription = null;
// }
// break;
//Authorize lastfm
case 'authorizeLastFM':
String username = args[0];
String password = args[1];
try {
LastFM lastFM = await LastFM.authenticateWithPasswordHash(
apiKey: 'b6ab5ae967bcd8b10b23f68f42493829',
apiSecret: '861b0dff9a8a574bec747f9dab8b82bf',
username: username,
passwordHash: password);
_scrobblenaut = Scrobblenaut(lastFM: lastFM);
_scrobblenautReady = true;
} catch (e) {
print(e);
}
break;
case 'disableLastFM':
_scrobblenaut = null;
_scrobblenautReady = false;
break;
}
return true;
}
@override
Future onTaskRemoved() async {
await onStop();
}
@override
Future onClose() async {
print('onClose');
await onStop();
}
Future onStop() async {
await _saveQueue();
_player.stop();
if (_eventSub != null) _eventSub.cancel();
if (_audioSessionSub != null) _audioSessionSub.cancel();
await super.onStop();
}
//Get queue save file path
Future<String> _getQueuePath() async {
Directory dir = await getApplicationDocumentsDirectory();
return p.join(dir.path, 'playback.json');
}
//Export queue to JSON
Future _saveQueue() async {
if (_queueIndex == 0 && _queue.length == 0) return;
String path = await _getQueuePath();
File f = File(path);
//Create if doesn't exist
if (!await File(path).exists()) {
f = await f.create();
}
Map data = {
'index': _queueIndex,
'queue': _queue.map<Map<String, dynamic>>((mi) => mi.toJson()).toList(),
'position': _player.position.inMilliseconds,
'queueSource': (queueSource ?? QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_loopMode ?? LoopMode.off)
};
await f.writeAsString(jsonEncode(data));
}
//Restore queue & playback info from path
Future _loadQueueFile() async {
File f = File(await _getQueuePath());
if (await f.exists()) {
Map<String, dynamic> json = jsonDecode(await f.readAsString());
this._queue = (json['queue'] ?? [])
.map<MediaItem>((mi) => MediaItem.fromJson(mi))
.toList();
this._queueIndex = json['index'] ?? 0;
this._lastPosition = Duration(milliseconds: json['position'] ?? 0);
this.queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
this._loopMode = LoopMode.values[(json['loopMode'] ?? 0)];
//Restore queue
if (_queue != null) {
await AudioServiceBackground.setQueue(_queue);
await _loadQueue();
await AudioServiceBackground.setMediaItem(mediaItem);
}
}
//Send restored queue source to ui
AudioServiceBackground.sendCustomEvent({
'action': 'onRestore',
'queueSource': (queueSource ?? QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_loopMode)
});
return true;
}
@override
Future onAddQueueItemAt(MediaItem mi, int index) async {
//-1 == play next
if (index == -1) index = _queueIndex + 1;
_queue.insert(index, mi);
await AudioServiceBackground.setQueue(_queue);
AudioSource _newSource = await _mediaItemToAudioSource(mi);
if (_newSource != null) await _audioSource.insert(index, _newSource);
_saveQueue();
}
//Add at end of queue
@override
Future onAddQueueItem(MediaItem mi) async {
if (_queue.indexWhere((m) => m.id == mi.id) != -1) return;
_queue.add(mi);
await AudioServiceBackground.setQueue(_queue);
AudioSource _newSource = await _mediaItemToAudioSource(mi);
if (_newSource != null) await _audioSource.add(_newSource);
_saveQueue();
}
@override
Future onPlayFromMediaId(String mediaId) async {
//Android auto load tracks
if (mediaId.startsWith(AndroidAuto.prefix)) {
AudioServiceBackground.sendCustomEvent({
'action': 'tracksAndroidAuto',
'id': mediaId.replaceFirst(AndroidAuto.prefix, '')
});
return;
}
//Does the same thing
await this.onSkipToQueueItem(mediaId);
}
}
//Seeker from audio_service example (why reinvent the wheel?)
//While holding seek button, will continuously seek
class Seeker {
final AudioPlayer player;
final Duration positionInterval;
final Duration stepInterval;
final MediaItem mediaItem;
bool _running = false;
Seeker(this.player, this.positionInterval, this.stepInterval, this.mediaItem);
Future start() async {
_running = true;
while (_running) {
Duration newPosition = player.position + positionInterval;
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
player.seek(newPosition);
await Future.delayed(stepInterval);
}
}
void stop() {
_running = false;
}
}