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 '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 KEY_INDEX = 'INDEX'; class AudioPlayerTask extends BackgroundAudioTask { final _audioPlayer = AudioPlayer(); MoorMusicDataSource _moorMusicDataSource; final _mediaItems = {}; List _originalPlaybackContext = []; List _playbackContext = []; 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(_index); } } @override Future onSkipToPrevious() async { if (_decrementIndex()) { await _audioPlayer.stop(); _startPlayback(_index); } } @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(); 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); _playbackContext = _getPermutatedSongs(_mediaItems, permutation); playbackIndex = index; AudioServiceBackground.setQueue(_playbackContext); _startPlayback(index); } Future _onAppLifecycleResumed() async { playbackIndex = playbackIndex; // AudioServiceBackground.setQueue(_playbackContext); } // 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()); } _originalPlaybackContext = mediaItems; return mediaItems; } // TODO: test // TODO: needs implementation for shuffle mode List _generateSongPermutation(List songs) { // permutation[i] = j; => song j is on the i-th position in the permutated list final permutation = []; for (var i = 0; i < songs.length; i++) { permutation.add(i); } 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, ); // TODO: needs implementation for shuffle mode (play first song) 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, );