import 'dart:async'; import 'dart:ui'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import 'package:moor/isolate.dart'; import 'package:moor/moor.dart'; import '../../domain/entities/shuffle_mode.dart'; import 'moor_music_data_source.dart'; const String INIT = 'INIT'; const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT'; const String APP_LIFECYCLE_RESUMED = 'APP_LIFECYCLE_RESUMED'; const String SET_SHUFFLE_MODE = 'SET_SHUFFLE_MODE'; const String KEY_INDEX = 'INDEX'; class AudioPlayerTask extends BackgroundAudioTask { final _audioPlayer = AudioPlayer(); MoorMusicDataSource _moorMusicDataSource; final _mediaItems = {}; List _originalPlaybackContext = []; List _playbackContext = []; ShuffleMode _shuffleMode = ShuffleMode.none; int _index = -1; int get playbackIndex => _index; set playbackIndex(int i) { print('setting index'); _index = i; AudioServiceBackground.sendCustomEvent({KEY_INDEX: _index}); } Duration _position; @override Future onStop() async { await _audioPlayer.stop(); await super.onStop(); } @override Future onAddQueueItem(MediaItem mediaItem) async { _mediaItems[mediaItem.id] = mediaItem; } @override Future onPlayFromMediaId(String mediaId) async { AudioServiceBackground.setState( controls: [pauseControl, stopControl], playing: true, processingState: AudioProcessingState.ready, ); await AudioServiceBackground.setMediaItem(_mediaItems[mediaId]); await _audioPlayer.setFilePath(mediaId); _audioPlayer.play(); } @override Future onPlay() async { AudioServiceBackground.setState( controls: [pauseControl, stopControl], processingState: AudioProcessingState.ready, updateTime: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), position: _position, playing: true, ); _audioPlayer.play(); } @override Future onPause() async { AudioServiceBackground.setState( controls: [playControl, stopControl], processingState: AudioProcessingState.ready, updateTime: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), position: _position, playing: false, ); await _audioPlayer.pause(); } @override Future onSkipToNext() async { if (_incrementIndex()) { await _audioPlayer.stop(); _startPlayback(playbackIndex); } } @override Future onSkipToPrevious() async { if (_decrementIndex()) { await _audioPlayer.stop(); _startPlayback(playbackIndex); } } @override Future onCustomAction(String name, arguments) async { switch (name) { case INIT: return _init(); case PLAY_WITH_CONTEXT: // arguments: [List, int] final args = arguments as List; final _context = List.from(args[0] as List); final index = args[1] as int; return _playWithContext(_context, index); case APP_LIFECYCLE_RESUMED: return _onAppLifecycleResumed(); case SET_SHUFFLE_MODE: return _setShuffleMode((arguments as String).toShuffleMode()); default: } } Future _init() async { print('AudioPlayerTask._init'); _audioPlayer.positionStream.listen((duration) => _position = duration); final connectPort = IsolateNameServer.lookupPortByName(MOOR_ISOLATE); final MoorIsolate moorIsolate = MoorIsolate.fromConnectPort(connectPort); final DatabaseConnection databaseConnection = await moorIsolate.connect(); _moorMusicDataSource = MoorMusicDataSource.connect(databaseConnection); } Future _playWithContext(List context, int index) async { print('AudioPlayerTask._playWithContext'); final _mediaItems = await _getMediaItemsFromPaths(context); final permutation = _generateSongPermutation(_mediaItems.length, index); _playbackContext = _getPermutatedSongs(_mediaItems, permutation); if (_shuffleMode == ShuffleMode.none) playbackIndex = index; else playbackIndex = 0; AudioServiceBackground.setQueue(_playbackContext); _startPlayback(playbackIndex); } Future _onAppLifecycleResumed() async { playbackIndex = playbackIndex; // AudioServiceBackground.setQueue(_playbackContext); } Future _setShuffleMode(ShuffleMode mode) async { _shuffleMode = mode; AudioServiceBackground.sendCustomEvent({SET_SHUFFLE_MODE: _shuffleMode.toString()}); // TODO: adapt queue } // TODO: test // TODO: optimize -> too slow for whole library Future> _getMediaItemsFromPaths(List paths) async { final mediaItems = []; for (final path in paths) { final song = await _moorMusicDataSource.getSongByPath(path); mediaItems.add(song.toMediaItem()); } // TODO: not good, side effects... _originalPlaybackContext = mediaItems; return mediaItems; } // TODO: test List _generateSongPermutation(int length, int startIndex) { // permutation[i] = j; => song j is on the i-th position in the permutated list List permutation; switch (_shuffleMode) { case ShuffleMode.none: permutation = List.generate(length, (i) => i); break; case ShuffleMode.standard: final tmp = List.generate(length, (i) => i) ..removeAt(startIndex) ..shuffle(); permutation = [startIndex] + tmp; break; case ShuffleMode.plus: break; } return permutation; } List _getPermutatedSongs( List songs, List permutation) { return List.generate( permutation.length, (index) => songs[permutation[index]]); } // TODO: cleanup and test Future _startPlayback(int index) async { // TODO: DRY AudioServiceBackground.setState( controls: [pauseControl, stopControl], playing: true, processingState: AudioProcessingState.ready, ); final _mediaItem = _playbackContext[index]; await AudioServiceBackground.setMediaItem(_mediaItem); await _audioPlayer.setFilePath(_mediaItem.id); // exploration: this works, but has to be used every time play() is called; maybe stateStream is the better option _audioPlayer.play().then((_) { if (_audioPlayer.processingState == ProcessingState.completed) onSkipToNext(); }); } bool _incrementIndex() { if (playbackIndex < _playbackContext.length - 1) { playbackIndex++; return true; } return false; } bool _decrementIndex() { if (playbackIndex > 0) { playbackIndex--; return true; } return false; } } MediaControl playControl = const MediaControl( androidIcon: 'drawable/ic_action_play_arrow', label: 'Play', action: MediaAction.play, ); MediaControl pauseControl = const MediaControl( androidIcon: 'drawable/ic_action_pause', label: 'Pause', action: MediaAction.pause, ); MediaControl stopControl = const MediaControl( androidIcon: 'drawable/ic_action_stop', label: 'Stop', action: MediaAction.stop, );