diff --git a/lib/domain/entities/player_state.dart b/lib/domain/entities/player_state.dart new file mode 100644 index 0000000..a4d5b53 --- /dev/null +++ b/lib/domain/entities/player_state.dart @@ -0,0 +1,22 @@ +class PlayerState { + PlayerState(this.playing, this.processingState); + + final bool playing; + + final ProcessingState processingState; + +} + +/// Enumerates the different processing states of a player. +enum ProcessingState { + /// The player has not loaded an [AudioSource]. + none, + /// The player is loading an [AudioSource]. + loading, + /// The player is buffering audio and unable to play. + buffering, + /// The player is has enough audio buffered and is able to play. + ready, + /// The player has reached the end of the audio. + completed, +} \ No newline at end of file diff --git a/lib/injection_container.dart b/lib/injection_container.dart index b6faef6..3ba60cd 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -2,6 +2,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:device_info/device_info.dart'; import 'package:flutter_audio_query/flutter_audio_query.dart'; import 'package:get_it/get_it.dart'; +import 'package:just_audio/just_audio.dart' as ja; import 'domain/repositories/audio_repository.dart'; import 'domain/repositories/music_data_repository.dart'; @@ -11,6 +12,9 @@ import 'presentation/state/navigation_store.dart'; import 'system/audio/audio_handler.dart'; import 'system/audio/audio_manager.dart'; import 'system/audio/audio_manager_contract.dart'; +import 'system/audio/audio_player_contract.dart'; +import 'system/audio/audio_player_impl.dart'; +import 'system/audio/queue_generator.dart'; import 'system/datasources/local_music_fetcher.dart'; import 'system/datasources/local_music_fetcher_contract.dart'; import 'system/datasources/moor_music_data_source.dart'; @@ -73,16 +77,26 @@ Future setupGetIt() async { ); getIt.registerLazySingleton(() => AudioManagerImpl(getIt())); + final AudioPlayer audioPlayer = AudioPlayerImpl( + ja.AudioPlayer(), + QueueGenerator(getIt()), + ); + getIt.registerLazySingleton(() => audioPlayer); + final _audioHandler = await AudioService.init( - builder: () => MyAudioHandler(getIt()), - config: AudioServiceConfig( - androidNotificationChannelName: 'mucke', - androidEnableQueue: true, - ), - ); + builder: () => MyAudioHandler(getIt(), getIt()), + config: AudioServiceConfig( + androidNotificationChannelName: 'mucke', + androidEnableQueue: true, + ), + ); getIt.registerLazySingleton(() => _audioHandler); + getIt.registerLazySingleton(() => QueueGenerator(getIt())); + // external + getIt.registerFactory(() => ja.AudioPlayer()); + getIt.registerLazySingleton(() => FlutterAudioQuery()); getIt.registerLazySingleton(() => DeviceInfoPlugin()); diff --git a/lib/presentation/widgets/time_progress_indicator.dart b/lib/presentation/widgets/time_progress_indicator.dart index 72ff70b..7554755 100644 --- a/lib/presentation/widgets/time_progress_indicator.dart +++ b/lib/presentation/widgets/time_progress_indicator.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:provider/provider.dart'; @@ -34,8 +36,8 @@ class TimeProgressIndicator extends StatelessWidget { ), alignment: Alignment.centerLeft, child: FractionallySizedBox( - widthFactor: - audioStore.currentPositionStream.value / duration, + widthFactor: min( + audioStore.currentPositionStream.value / duration, 1.0), heightFactor: 1.0, child: Container( height: double.infinity, diff --git a/lib/system/audio/audio_handler.dart b/lib/system/audio/audio_handler.dart index 74bba18..e20e31b 100644 --- a/lib/system/audio/audio_handler.dart +++ b/lib/system/audio/audio_handler.dart @@ -1,79 +1,45 @@ import 'dart:math'; import 'package:audio_service/audio_service.dart'; -import 'package:just_audio/just_audio.dart'; import 'package:logging/logging.dart'; +import '../../domain/entities/player_state.dart'; import '../../domain/entities/shuffle_mode.dart'; import '../datasources/music_data_source_contract.dart'; -import '../models/queue_item.dart'; import '../models/song_model.dart'; -import 'queue_generator.dart'; - -const String KEY_INDEX = 'INDEX'; -const String SHUFFLE_MODE = 'SHUFFLE_MODE'; - -const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT'; -const String INIT = 'INIT'; -const String APP_LIFECYCLE_RESUMED = 'APP_LIFECYCLE_RESUMED'; -const String SHUFFLE_ALL = 'SHUFFLE_ALL'; -const String SET_SHUFFLE_MODE = 'SET_SHUFFLE_MODE'; -const String MOVE_QUEUE_ITEM = 'MOVE_QUEUE_ITEM'; -const String REMOVE_QUEUE_ITEM = 'REMOVE_QUEUE_ITEM'; +import 'audio_player_contract.dart'; +import 'stream_constants.dart'; class MyAudioHandler extends BaseAudioHandler { - MyAudioHandler(this._musicDataSource); + MyAudioHandler(this._musicDataSource, this._audioPlayer) { + _audioPlayer.queueStream.listen((event) { + _handleSetQueue(event); + }); + _audioPlayer.currentIndexStream.listen((event) => _handleIndexChange(event)); - final _audioPlayer = AudioPlayer(); + _audioPlayer.currentSongStream.listen((songModel) { + mediaItemSubject.add(songModel.toMediaItem()); + }); + + _audioPlayer.playerStateStream.listen((event) { + _handlePlayerState(event); + }); + + _audioPlayer.shuffleModeStream.listen((shuffleMode) { + customEventSubject.add({SHUFFLE_MODE: shuffleMode}); + }); + } + + final AudioPlayer _audioPlayer; final MusicDataSource _musicDataSource; - QueueGenerator queueGenerator; - - // TODO: confusing naming - List originalPlaybackContext = []; - List playbackContext = []; - - // TODO: this is not trivial: queue is loaded by audioplayer - // this reference enables direct manipulation of the loaded queue - ConcatenatingAudioSource _queue; - List mediaItemQueue; - - ShuffleMode _shuffleMode = ShuffleMode.none; - - ShuffleMode get shuffleMode => _shuffleMode; - set shuffleMode(ShuffleMode s) { - _shuffleMode = s; - customEventSubject.add({SHUFFLE_MODE: s}); - } - - int _playbackIndex = -1; - int get playbackIndex => _playbackIndex; - set playbackIndex(int i) { - _log.info('index: $i'); - if (i != null) { - _playbackIndex = i; - mediaItemSubject.add(mediaItemQueue[i]); - customEventSubject.add({KEY_INDEX: i}); - - playbackStateSubject.add(playbackState.value.copyWith( - controls: [ - MediaControl.skipToPrevious, - MediaControl.pause, - MediaControl.skipToNext - ], - playing: _audioPlayer.playing, - processingState: AudioProcessingState.ready, - updatePosition: _audioPlayer.position, - )); - } - } static final _log = Logger('AudioHandler'); @override Future stop() async { await _audioPlayer.stop(); - await _audioPlayer.dispose(); + // await _audioPlayer.dispose(); await super.stop(); } @@ -99,16 +65,12 @@ class MyAudioHandler extends BaseAudioHandler { @override Future addQueueItem(MediaItem mediaItem) async { - await _queue.add(AudioSource.uri(Uri.file(mediaItem.id))); - mediaItemQueue.add(mediaItem); - handleSetQueue(mediaItemQueue); + _audioPlayer.addToQueue(SongModel.fromMediaItem(mediaItem)); } @override Future customAction(String name, Map arguments) async { switch (name) { - case INIT: - return init(); case PLAY_WITH_CONTEXT: final context = arguments['CONTEXT'] as List; final index = arguments['INDEX'] as int; @@ -127,161 +89,84 @@ class MyAudioHandler extends BaseAudioHandler { } } - Future handleSetQueue(List mediaItemQueue) async { - queueSubject.add(mediaItemQueue); - final songModels = - mediaItemQueue.map((e) => SongModel.fromMediaItem(e)).toList(); - _musicDataSource.setQueue(songModels); - } - - Future init() async { - print('AudioPlayerTask.init'); - _audioPlayer.playerStateStream.listen((event) => handlePlayerState(event)); - _audioPlayer.currentIndexStream.listen((event) => playbackIndex = event); - _audioPlayer.sequenceStateStream - .listen((event) => handleSequenceState(event)); - - queueGenerator = QueueGenerator(_musicDataSource); - } - Future playWithContext(List context, int index) async { - final mediaItems = await queueGenerator.getMediaItemsFromPaths(context); - playPlaylist(mediaItems, index); + final songs = []; + for (final path in context) { + final song = await _musicDataSource.getSongByPath(path); + songs.add(song); + } + + _audioPlayer.playSongList(songs, index); } Future onAppLifecycleResumed() async { - customEventSubject.add({SHUFFLE_MODE: shuffleMode}); - customEventSubject.add({KEY_INDEX: playbackIndex}); + // customEventSubject.add({SHUFFLE_MODE: shuffleMode}); + // customEventSubject.add({KEY_INDEX: playbackIndex}); } Future setCustomShuffleMode(ShuffleMode mode) async { - shuffleMode = mode; - - final QueueItem currentQueueItem = playbackContext[playbackIndex]; - final int index = currentQueueItem.originalIndex; - playbackContext = await queueGenerator.generateQueue( - shuffleMode, originalPlaybackContext, index); - mediaItemQueue = playbackContext.map((e) => e.mediaItem).toList(); - - // FIXME: this does not react correctly when inserted track is currently played - handleSetQueue(mediaItemQueue); - - final newQueue = queueGenerator.mediaItemsToAudioSource(mediaItemQueue); - _updateQueue(newQueue, currentQueueItem); - } - - void _updateQueue( - ConcatenatingAudioSource newQueue, QueueItem currentQueueItem) { - final int index = currentQueueItem.originalIndex; - - _queue.removeRange(0, playbackIndex); - _queue.removeRange(1, _queue.length); - - if (shuffleMode == ShuffleMode.none) { - switch (currentQueueItem.type) { - case QueueItemType.standard: - _queue.insertAll(0, newQueue.children.sublist(0, index)); - _queue.addAll(newQueue.children.sublist(index + 1)); - playbackIndex = index; - break; - case QueueItemType.predecessor: - _queue.insertAll(0, newQueue.children.sublist(0, index)); - _queue.addAll(newQueue.children.sublist(index)); - playbackIndex = index; - break; - case QueueItemType.successor: - _queue.insertAll(0, newQueue.children.sublist(0, index + 1)); - _queue.addAll(newQueue.children.sublist(index + 1)); - playbackIndex = index; - break; - } - } else { - _queue.addAll(newQueue.children.sublist(1)); - } + _audioPlayer.setShuffleMode(mode, true); } Future shuffleAll() async { - shuffleMode = ShuffleMode.standard; + _audioPlayer.setShuffleMode(ShuffleMode.plus, false); + final List songs = await _musicDataSource.getSongs(); - final List mediaItems = - songs.map((song) => song.toMediaItem()).toList(); - final rng = Random(); - final index = rng.nextInt(mediaItems.length); + final index = rng.nextInt(songs.length); - playPlaylist(mediaItems, index); - } - - Future playPlaylist(List mediaItems, int index) async { - final firstMediaItem = mediaItems.sublist(index, index + 1); - mediaItemQueue = firstMediaItem; - handleSetQueue(firstMediaItem); - _queue = queueGenerator.mediaItemsToAudioSource(firstMediaItem); - _audioPlayer.play(); - await _audioPlayer.load(_queue, initialIndex: 0); - - - originalPlaybackContext = mediaItems; - - playbackContext = - await queueGenerator.generateQueue(shuffleMode, mediaItems, index); - mediaItemQueue = playbackContext.map((e) => e.mediaItem).toList(); - - handleSetQueue(mediaItemQueue); - final int splitIndex = shuffleMode == ShuffleMode.none ? index : 0; - final newQueue = queueGenerator.mediaItemsToAudioSource(mediaItemQueue); - _queue.insertAll(0, newQueue.children.sublist(0, splitIndex)); - _queue.addAll(newQueue.children.sublist(splitIndex + 1, newQueue.length)); + _audioPlayer.playSongList(songs, index); } Future moveQueueItem(int oldIndex, int newIndex) async { - _log.info('move: $oldIndex -> $newIndex'); - final MediaItem mediaItem = mediaItemQueue.removeAt(oldIndex); - final index = newIndex < oldIndex ? newIndex : newIndex - 1; - mediaItemQueue.insert(index, mediaItem); - handleSetQueue(mediaItemQueue); - _queue.move(oldIndex, index); + _audioPlayer.moveQueueItem(oldIndex, newIndex); } Future removeQueueIndex(int index) async { - mediaItemQueue.removeAt(index); - handleSetQueue(mediaItemQueue); - _queue.removeAt(index); + _audioPlayer.removeQueueIndex(index); } - void handlePlayerState(PlayerState ps) { - _log.info('handlePlayerState called'); - if (ps.processingState == ProcessingState.ready && ps.playing) { + void _handleSetQueue(List queue) { + _musicDataSource.setQueue(queue); + + final mediaItems = queue.map((e) => e.toMediaItem()).toList(); + queueSubject.add(mediaItems); + } + + void _handleIndexChange(int index) { + _log.info('index: $index'); + if (index != null) { + customEventSubject.add({KEY_INDEX: index}); + playbackStateSubject.add(playbackState.value.copyWith( controls: [ MediaControl.skipToPrevious, MediaControl.pause, - MediaControl.skipToNext + MediaControl.skipToNext, ], - playing: true, + playing: _audioPlayer.playerStateStream.value.playing, processingState: AudioProcessingState.ready, - updatePosition: _audioPlayer.position, - )); - } else if (ps.processingState == ProcessingState.ready && !ps.playing) { - playbackStateSubject.add(playbackState.value.copyWith( - controls: [ - MediaControl.skipToPrevious, - MediaControl.play, - MediaControl.skipToNext - ], - processingState: AudioProcessingState.ready, - updatePosition: _audioPlayer.position, - playing: false, + updatePosition: const Duration(milliseconds: 0), // _audioPlayer.positionStream.value, )); } } - // TODO: this can only be a temporary solution! gets called too often. - void handleSequenceState(SequenceState st) { - _log.info('handleSequenceState called'); - if (0 <= playbackIndex && playbackIndex < playbackContext.length) { - _log.info('handleSequenceState: setting MediaItem'); - mediaItemSubject.add(mediaItemQueue[playbackIndex]); + void _handlePlayerState(PlayerState ps) { + _log.info('handlePlayerState called'); + if (ps.processingState == ProcessingState.ready && ps.playing) { + playbackStateSubject.add(playbackState.value.copyWith( + controls: [MediaControl.skipToPrevious, MediaControl.pause, MediaControl.skipToNext], + playing: true, + processingState: AudioProcessingState.ready, + updatePosition: _audioPlayer.positionStream.value, + )); + } else if (ps.processingState == ProcessingState.ready && !ps.playing) { + playbackStateSubject.add(playbackState.value.copyWith( + controls: [MediaControl.skipToPrevious, MediaControl.play, MediaControl.skipToNext], + processingState: AudioProcessingState.ready, + updatePosition: _audioPlayer.positionStream.value, + playing: false, + )); } } } diff --git a/lib/system/audio/audio_manager.dart b/lib/system/audio/audio_manager.dart index ce877f0..ff80d9e 100644 --- a/lib/system/audio/audio_manager.dart +++ b/lib/system/audio/audio_manager.dart @@ -6,15 +6,13 @@ import '../../domain/entities/playback_state.dart' as entity; import '../../domain/entities/shuffle_mode.dart'; import '../models/playback_state_model.dart'; import '../models/song_model.dart'; -import 'audio_handler.dart'; import 'audio_manager_contract.dart'; +import 'stream_constants.dart'; typedef Conversion = T Function(S); class AudioManagerImpl implements AudioManager { AudioManagerImpl(this._audioHandler) { - _audioHandler.customAction(INIT, null); - _audioHandler.customEventStream.listen((event) { final data = event as Map; if (data.containsKey(KEY_INDEX)) { diff --git a/lib/system/audio/audio_player_contract.dart b/lib/system/audio/audio_player_contract.dart new file mode 100644 index 0000000..a9f8ef3 --- /dev/null +++ b/lib/system/audio/audio_player_contract.dart @@ -0,0 +1,33 @@ +import 'package:rxdart/rxdart.dart'; + +import '../../domain/entities/shuffle_mode.dart'; +import '../models/player_state_model.dart'; +import '../models/queue_item.dart'; +import '../models/song_model.dart'; + +abstract class AudioPlayer { + ValueStream get currentIndexStream; + ValueStream get currentSongStream; + ValueStream get playerStateStream; + ValueStream get positionStream; + ValueStream> get queueStream; + ValueStream get shuffleModeStream; + + Future play(); + Future pause(); + Future stop(); + Future seekToNext(); + Future seekToPrevious(); + Future dispose(); + + Future loadQueue(List queue); + Future addToQueue(SongModel song); + Future moveQueueItem(int oldIndex, int newIndex); + Future removeQueueIndex(int index); + Future setIndex(int index); + + + Future setShuffleMode(ShuffleMode shuffleMode, bool updateQueue); + + Future playSongList(List songs, int startIndex); +} diff --git a/lib/system/audio/audio_player_impl.dart b/lib/system/audio/audio_player_impl.dart new file mode 100644 index 0000000..0d3784f --- /dev/null +++ b/lib/system/audio/audio_player_impl.dart @@ -0,0 +1,191 @@ +import 'package:just_audio/just_audio.dart' as ja; +import 'package:rxdart/rxdart.dart'; + +import '../../domain/entities/shuffle_mode.dart'; +import '../models/player_state_model.dart'; +import '../models/queue_item.dart'; +import '../models/song_model.dart'; +import 'audio_player_contract.dart'; +import 'queue_generator.dart'; + +class AudioPlayerImpl implements AudioPlayer { + AudioPlayerImpl(this._audioPlayer, this._queueGenerator) { + _audioPlayer.currentIndexStream.listen((event) { + _currentIndexSubject.add(event); + _currentSongSubject.add(_queueSubject.value[event]); + }); + + _audioPlayer.positionStream.listen((event) { + _positionSubject.add(event); + }); + + _audioPlayer.playerStateStream.listen((event) { + _playerStateSubject.add(PlayerStateModel.fromJAPlayerState(event)); + }); + + _queueSubject.listen((event) { + _currentSongSubject.add(event[_currentIndexSubject.value]); + }); + } + + final ja.AudioPlayer _audioPlayer; + ja.ConcatenatingAudioSource _audioSource; + final QueueGenerator _queueGenerator; + + List _inputQueue; + List _queue; + + final BehaviorSubject _currentIndexSubject = BehaviorSubject(); + final BehaviorSubject _currentSongSubject = BehaviorSubject(); + final BehaviorSubject _playerStateSubject = BehaviorSubject(); + final BehaviorSubject _positionSubject = BehaviorSubject(); + final BehaviorSubject> _queueSubject = BehaviorSubject.seeded([]); + final BehaviorSubject _shuffleModeSubject = BehaviorSubject.seeded(ShuffleMode.none); + + @override + ValueStream get currentIndexStream => _currentIndexSubject.stream; + + @override + ValueStream get currentSongStream => _currentSongSubject.stream; + + @override + ValueStream get playerStateStream => _playerStateSubject.stream; + + @override + ValueStream get positionStream => _positionSubject.stream; + + @override + ValueStream> get queueStream => _queueSubject.stream; + + @override + ValueStream get shuffleModeStream => _shuffleModeSubject.stream; + + @override + Future dispose() async { + await _audioPlayer.dispose(); + } + + @override + Future loadQueue(List queue) { + // TODO: implement loadQueue + throw UnimplementedError(); + } + + @override + Future pause() async { + await _audioPlayer.pause(); + } + + @override + Future play() async { + await _audioPlayer.play(); + } + + @override + Future playSongList(List songs, int startIndex) async { + _inputQueue = songs; + + final firstSong = songs.sublist(startIndex, startIndex + 1); + _queueSubject.add(firstSong); + _audioSource = _queueGenerator.songModelsToAudioSource(firstSong); + _audioPlayer.play(); + await _audioPlayer.load(_audioSource, initialIndex: 0); + + _queue = await _queueGenerator.generateQueue(_shuffleModeSubject.value, songs, startIndex); + final songModelQueue = _queue.map((e) => e.song).toList(); + _queueSubject.add(songModelQueue); + + final int splitIndex = _shuffleModeSubject.value == ShuffleMode.none ? startIndex : 0; + final newQueue = _queueGenerator.songModelsToAudioSource(songModelQueue); + _audioSource.insertAll(0, newQueue.children.sublist(0, splitIndex)); + _audioSource.addAll(newQueue.children.sublist(splitIndex + 1, newQueue.length)); + } + + @override + Future seekToNext() async { + await _audioPlayer.seekToNext(); + } + + @override + Future seekToPrevious() async { + await _audioPlayer.seekToPrevious(); + } + + @override + Future setIndex(int index) async { + await _audioPlayer.seek(const Duration(seconds: 0), index: index); + } + + @override + Future stop() async { + _audioPlayer.stop(); + } + + @override + Future addToQueue(SongModel song) async { + await _audioSource.add(ja.AudioSource.uri(Uri.file(song.path))); + _queue.add(QueueItem(song, originalIndex: -1, type: QueueItemType.added)); + _queueSubject.add(_queue.map((e) => e.song).toList()); + } + + @override + Future moveQueueItem(int oldIndex, int newIndex) async { + final QueueItem queueItem = _queue.removeAt(oldIndex); + final index = newIndex < oldIndex ? newIndex : newIndex - 1; + _queue.insert(index, queueItem); + _queueSubject.add(_queue.map((e) => e.song).toList()); + await _audioSource.move(oldIndex, index); + } + + @override + Future removeQueueIndex(int index) async { + _queue.removeAt(index); + _queueSubject.add(_queue.map((e) => e.song).toList()); + await _audioSource.removeAt(index); + } + + @override + Future setShuffleMode(ShuffleMode shuffleMode, bool updateQueue) async { + _shuffleModeSubject.add(shuffleMode); + + if (updateQueue) { + final QueueItem currentQueueItem = _queue[_currentIndexSubject.value]; + final int index = currentQueueItem.originalIndex; + _queue = await _queueGenerator.generateQueue(shuffleMode, _inputQueue, index); + // TODO: maybe refactor _queue to a subject and listen for changes + final songModelQueue = _queue.map((e) => e.song).toList(); + _queueSubject.add(songModelQueue); + + final newQueue = _queueGenerator.songModelsToAudioSource(songModelQueue); + _updateQueue(newQueue, currentQueueItem); + } + } + + void _updateQueue(ja.ConcatenatingAudioSource newQueue, QueueItem currentQueueItem) { + final int index = currentQueueItem.originalIndex; + + _audioSource.removeRange(0, _currentIndexSubject.value); + _audioSource.removeRange(1, _audioSource.length); + + if (_shuffleModeSubject.value == ShuffleMode.none) { + switch (currentQueueItem.type) { + case QueueItemType.added: + case QueueItemType.standard: + _audioSource.insertAll(0, newQueue.children.sublist(0, index)); + _audioSource.addAll(newQueue.children.sublist(index + 1)); + break; + case QueueItemType.predecessor: + _audioSource.insertAll(0, newQueue.children.sublist(0, index)); + _audioSource.addAll(newQueue.children.sublist(index)); + break; + case QueueItemType.successor: + _audioSource.insertAll(0, newQueue.children.sublist(0, index + 1)); + _audioSource.addAll(newQueue.children.sublist(index + 1)); + break; + } + _currentIndexSubject.add(index); + } else { + _audioSource.addAll(newQueue.children.sublist(1)); + } + } +} diff --git a/lib/system/audio/queue_generator.dart b/lib/system/audio/queue_generator.dart index 4fbdd72..1e3831d 100644 --- a/lib/system/audio/queue_generator.dart +++ b/lib/system/audio/queue_generator.dart @@ -11,35 +11,22 @@ class QueueGenerator { final MusicDataSource _musicDataSource; - // TODO: test - // TODO: optimize -> too slow for whole library - // fetching all songs together and preparing playback takes ~500ms compared to ~10.000ms individually - Future> getMediaItemsFromPaths(List paths) async { - final mediaItems = []; - for (final path in paths) { - final song = await _musicDataSource.getSongByPath(path); - mediaItems.add(song.toMediaItem()); - } - - return mediaItems; - } - Future> generateQueue( ShuffleMode shuffleMode, - List mediaItems, + List songModels, int startIndex, ) async { List queue; switch (shuffleMode) { case ShuffleMode.none: - queue = _generateNormalQueue(mediaItems); + queue = _generateNormalQueue(songModels); break; case ShuffleMode.standard: - queue = _generateShuffleQueue(mediaItems, startIndex); + queue = _generateShuffleQueue(songModels, startIndex); break; case ShuffleMode.plus: - queue = await _generateShufflePlusQueue(mediaItems, startIndex); + queue = await _generateShufflePlusQueue(songModels, startIndex); } return queue; @@ -47,55 +34,60 @@ class QueueGenerator { ConcatenatingAudioSource mediaItemsToAudioSource(List mediaItems) { return ConcatenatingAudioSource( - children: mediaItems - .map((MediaItem m) => AudioSource.uri(Uri.file(m.id))) - .toList()); + children: mediaItems.map((MediaItem m) => AudioSource.uri(Uri.file(m.id))).toList(), + ); } - List _generateNormalQueue(List mediaItems) { + ConcatenatingAudioSource songModelsToAudioSource(List songModels) { + return ConcatenatingAudioSource( + children: songModels.map((SongModel m) => AudioSource.uri(Uri.file(m.path))).toList(), + ); + } + + List _generateNormalQueue(List songs) { return List.generate( - mediaItems.length, + songs.length, (i) => QueueItem( - mediaItems[i], + songs[i], originalIndex: i, ), ); } List _generateShuffleQueue( - List mediaItems, + List songs, int startIndex, ) { final List queue = List.generate( - mediaItems.length, + songs.length, (i) => QueueItem( - mediaItems[i], + songs[i], originalIndex: i, ), ); queue.removeAt(startIndex); queue.shuffle(); final first = QueueItem( - mediaItems[startIndex], + songs[startIndex], originalIndex: startIndex, ); return [first] + queue; } Future> _generateShufflePlusQueue( - List mediaItems, + List songs, int startIndex, ) async { final List queue = await _getQueueItemWithLinks( - mediaItems[startIndex], + songs[startIndex], startIndex, ); final List indices = []; // filter mediaitem list // TODO: multiply higher rated songs - for (var i = 0; i < mediaItems.length; i++) { - if (i != startIndex && !(mediaItems[i].extras['blocked'] as bool)) { + for (var i = 0; i < songs.length; i++) { + if (i != startIndex && !songs[i].blocked) { indices.add(i); } } @@ -104,9 +96,9 @@ class QueueGenerator { for (var i = 0; i < indices.length; i++) { final int index = indices[i]; - final MediaItem mediaItem = mediaItems[index]; + final SongModel song = songs[index]; - queue.addAll(await _getQueueItemWithLinks(mediaItem, index)); + queue.addAll(await _getQueueItemWithLinks(song, index)); } return queue; @@ -114,13 +106,13 @@ class QueueGenerator { // TODO: naming things is hard Future> _getQueueItemWithLinks( - MediaItem mediaItem, + SongModel song, int index, ) async { final List queueItems = []; - final predecessors = await _getPredecessors(mediaItem); - final successors = await _getSuccessors(mediaItem); + final predecessors = await _getPredecessors(song); + final successors = await _getSuccessors(song); for (final p in predecessors) { queueItems.add(QueueItem( @@ -131,7 +123,7 @@ class QueueGenerator { } queueItems.add(QueueItem( - mediaItem, + song, originalIndex: index, )); @@ -146,31 +138,27 @@ class QueueGenerator { return queueItems; } - Future> _getPredecessors(MediaItem mediaItem) async { - final List mediaItems = []; - MediaItem currentMediaItem = mediaItem; + Future> _getPredecessors(SongModel song) async { + final List songs = []; + SongModel currentSong = song; - while (currentMediaItem.previous != null) { - currentMediaItem = - (await _musicDataSource.getSongByPath(currentMediaItem.previous)) - .toMediaItem(); - mediaItems.add(currentMediaItem); + while (currentSong.previous != null) { + currentSong = await _musicDataSource.getSongByPath(currentSong.previous); + songs.add(currentSong); } - return mediaItems.reversed.toList(); + return songs.reversed.toList(); } - Future> _getSuccessors(MediaItem mediaItem) async { - final List mediaItems = []; - MediaItem currentMediaItem = mediaItem; + Future> _getSuccessors(SongModel song) async { + final List songs = []; + SongModel currentSong = song; - while (currentMediaItem.next != null) { - currentMediaItem = - (await _musicDataSource.getSongByPath(currentMediaItem.next)) - .toMediaItem(); - mediaItems.add(currentMediaItem); + while (currentSong.next != null) { + currentSong = await _musicDataSource.getSongByPath(currentSong.next); + songs.add(currentSong); } - return mediaItems.toList(); + return songs.toList(); } } diff --git a/lib/system/audio/stream_constants.dart b/lib/system/audio/stream_constants.dart new file mode 100644 index 0000000..5d54f35 --- /dev/null +++ b/lib/system/audio/stream_constants.dart @@ -0,0 +1,9 @@ +const String KEY_INDEX = 'INDEX'; +const String SHUFFLE_MODE = 'SHUFFLE_MODE'; + +const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT'; +const String APP_LIFECYCLE_RESUMED = 'APP_LIFECYCLE_RESUMED'; +const String SHUFFLE_ALL = 'SHUFFLE_ALL'; +const String SET_SHUFFLE_MODE = 'SET_SHUFFLE_MODE'; +const String MOVE_QUEUE_ITEM = 'MOVE_QUEUE_ITEM'; +const String REMOVE_QUEUE_ITEM = 'REMOVE_QUEUE_ITEM'; \ No newline at end of file diff --git a/lib/system/models/player_state_model.dart b/lib/system/models/player_state_model.dart new file mode 100644 index 0000000..eb90f52 --- /dev/null +++ b/lib/system/models/player_state_model.dart @@ -0,0 +1,48 @@ +import 'package:just_audio/just_audio.dart' as ja; + +import '../../domain/entities/player_state.dart'; + +class PlayerStateModel extends PlayerState { + PlayerStateModel(bool playing, ProcessingState processingState) + : super(playing, processingState); + + factory PlayerStateModel.fromJAPlayerState(ja.PlayerState playerState) => + PlayerStateModel( + playerState.playing, + playerState.processingState.toProcessingState(), + ); +} + +// extension JAProcessingStateExt on ProcessingState { +// ProcessingState fromJAProcessingState(ja.ProcessingState processingState) { +// switch (processingState) { +// case ja.ProcessingState.loading: +// return ProcessingState.loading; +// case ja.ProcessingState.buffering: +// return ProcessingState.buffering; +// case ja.ProcessingState.ready: +// return ProcessingState.ready; +// case ja.ProcessingState.completed: +// return ProcessingState.completed; +// default: +// return ProcessingState.none; +// } +// } +// } + +extension ProcessingStateExt on ja.ProcessingState { + ProcessingState toProcessingState() { + switch (this) { + case ja.ProcessingState.loading: + return ProcessingState.loading; + case ja.ProcessingState.buffering: + return ProcessingState.buffering; + case ja.ProcessingState.ready: + return ProcessingState.ready; + case ja.ProcessingState.completed: + return ProcessingState.completed; + default: + return ProcessingState.none; + } + } +} diff --git a/lib/system/models/queue_item.dart b/lib/system/models/queue_item.dart index 3f96502..c819d8f 100644 --- a/lib/system/models/queue_item.dart +++ b/lib/system/models/queue_item.dart @@ -1,11 +1,15 @@ -import 'package:audio_service/audio_service.dart'; +import 'song_model.dart'; class QueueItem { - QueueItem(this.mediaItem, {this.originalIndex, this.type = QueueItemType.standard}); + QueueItem( + this.song, { + this.originalIndex, + this.type = QueueItemType.standard, + }); - final MediaItem mediaItem; + final SongModel song; final int originalIndex; final QueueItemType type; } -enum QueueItemType { standard, predecessor, successor } +enum QueueItemType { standard, predecessor, successor, added }