refactored audio_handler
This commit is contained in:
parent
4f8cd4b9ee
commit
9c58244bb4
11 changed files with 450 additions and 256 deletions
22
lib/domain/entities/player_state.dart
Normal file
22
lib/domain/entities/player_state.dart
Normal file
|
@ -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,
|
||||
}
|
|
@ -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<void> setupGetIt() async {
|
|||
);
|
||||
getIt.registerLazySingleton<AudioManager>(() => AudioManagerImpl(getIt()));
|
||||
|
||||
final AudioPlayer audioPlayer = AudioPlayerImpl(
|
||||
ja.AudioPlayer(),
|
||||
QueueGenerator(getIt()),
|
||||
);
|
||||
getIt.registerLazySingleton<AudioPlayer>(() => 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>(() => _audioHandler);
|
||||
|
||||
getIt.registerLazySingleton<QueueGenerator>(() => QueueGenerator(getIt()));
|
||||
|
||||
// external
|
||||
getIt.registerFactory<ja.AudioPlayer>(() => ja.AudioPlayer());
|
||||
|
||||
getIt.registerLazySingleton<FlutterAudioQuery>(() => FlutterAudioQuery());
|
||||
|
||||
getIt.registerLazySingleton<DeviceInfoPlugin>(() => DeviceInfoPlugin());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<MediaItem> originalPlaybackContext = <MediaItem>[];
|
||||
List<QueueItem> playbackContext = <QueueItem>[];
|
||||
|
||||
// TODO: this is not trivial: queue is loaded by audioplayer
|
||||
// this reference enables direct manipulation of the loaded queue
|
||||
ConcatenatingAudioSource _queue;
|
||||
List<MediaItem> 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<void> stop() async {
|
||||
await _audioPlayer.stop();
|
||||
await _audioPlayer.dispose();
|
||||
// await _audioPlayer.dispose();
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
|
@ -99,16 +65,12 @@ class MyAudioHandler extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> 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<void> customAction(String name, Map<String, dynamic> arguments) async {
|
||||
switch (name) {
|
||||
case INIT:
|
||||
return init();
|
||||
case PLAY_WITH_CONTEXT:
|
||||
final context = arguments['CONTEXT'] as List<String>;
|
||||
final index = arguments['INDEX'] as int;
|
||||
|
@ -127,161 +89,84 @@ class MyAudioHandler extends BaseAudioHandler {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> handleSetQueue(List<MediaItem> mediaItemQueue) async {
|
||||
queueSubject.add(mediaItemQueue);
|
||||
final songModels =
|
||||
mediaItemQueue.map((e) => SongModel.fromMediaItem(e)).toList();
|
||||
_musicDataSource.setQueue(songModels);
|
||||
}
|
||||
|
||||
Future<void> 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<void> playWithContext(List<String> context, int index) async {
|
||||
final mediaItems = await queueGenerator.getMediaItemsFromPaths(context);
|
||||
playPlaylist(mediaItems, index);
|
||||
final songs = <SongModel>[];
|
||||
for (final path in context) {
|
||||
final song = await _musicDataSource.getSongByPath(path);
|
||||
songs.add(song);
|
||||
}
|
||||
|
||||
_audioPlayer.playSongList(songs, index);
|
||||
}
|
||||
|
||||
Future<void> onAppLifecycleResumed() async {
|
||||
customEventSubject.add({SHUFFLE_MODE: shuffleMode});
|
||||
customEventSubject.add({KEY_INDEX: playbackIndex});
|
||||
// customEventSubject.add({SHUFFLE_MODE: shuffleMode});
|
||||
// customEventSubject.add({KEY_INDEX: playbackIndex});
|
||||
}
|
||||
|
||||
Future<void> 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<void> shuffleAll() async {
|
||||
shuffleMode = ShuffleMode.standard;
|
||||
_audioPlayer.setShuffleMode(ShuffleMode.plus, false);
|
||||
|
||||
final List<SongModel> songs = await _musicDataSource.getSongs();
|
||||
final List<MediaItem> 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<void> playPlaylist(List<MediaItem> 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<void> 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<void> 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<SongModel> 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<S, T> = T Function(S);
|
||||
|
||||
class AudioManagerImpl implements AudioManager {
|
||||
AudioManagerImpl(this._audioHandler) {
|
||||
_audioHandler.customAction(INIT, null);
|
||||
|
||||
_audioHandler.customEventStream.listen((event) {
|
||||
final data = event as Map<String, dynamic>;
|
||||
if (data.containsKey(KEY_INDEX)) {
|
||||
|
|
33
lib/system/audio/audio_player_contract.dart
Normal file
33
lib/system/audio/audio_player_contract.dart
Normal file
|
@ -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<int> get currentIndexStream;
|
||||
ValueStream<SongModel> get currentSongStream;
|
||||
ValueStream<PlayerStateModel> get playerStateStream;
|
||||
ValueStream<Duration> get positionStream;
|
||||
ValueStream<List<SongModel>> get queueStream;
|
||||
ValueStream<ShuffleMode> get shuffleModeStream;
|
||||
|
||||
Future<void> play();
|
||||
Future<void> pause();
|
||||
Future<void> stop();
|
||||
Future<void> seekToNext();
|
||||
Future<void> seekToPrevious();
|
||||
Future<void> dispose();
|
||||
|
||||
Future<void> loadQueue(List<QueueItem> queue);
|
||||
Future<void> addToQueue(SongModel song);
|
||||
Future<void> moveQueueItem(int oldIndex, int newIndex);
|
||||
Future<void> removeQueueIndex(int index);
|
||||
Future<void> setIndex(int index);
|
||||
|
||||
|
||||
Future<void> setShuffleMode(ShuffleMode shuffleMode, bool updateQueue);
|
||||
|
||||
Future<void> playSongList(List<SongModel> songs, int startIndex);
|
||||
}
|
191
lib/system/audio/audio_player_impl.dart
Normal file
191
lib/system/audio/audio_player_impl.dart
Normal file
|
@ -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<SongModel> _inputQueue;
|
||||
List<QueueItem> _queue;
|
||||
|
||||
final BehaviorSubject<int> _currentIndexSubject = BehaviorSubject();
|
||||
final BehaviorSubject<SongModel> _currentSongSubject = BehaviorSubject();
|
||||
final BehaviorSubject<PlayerStateModel> _playerStateSubject = BehaviorSubject();
|
||||
final BehaviorSubject<Duration> _positionSubject = BehaviorSubject();
|
||||
final BehaviorSubject<List<SongModel>> _queueSubject = BehaviorSubject.seeded([]);
|
||||
final BehaviorSubject<ShuffleMode> _shuffleModeSubject = BehaviorSubject.seeded(ShuffleMode.none);
|
||||
|
||||
@override
|
||||
ValueStream<int> get currentIndexStream => _currentIndexSubject.stream;
|
||||
|
||||
@override
|
||||
ValueStream<SongModel> get currentSongStream => _currentSongSubject.stream;
|
||||
|
||||
@override
|
||||
ValueStream<PlayerStateModel> get playerStateStream => _playerStateSubject.stream;
|
||||
|
||||
@override
|
||||
ValueStream<Duration> get positionStream => _positionSubject.stream;
|
||||
|
||||
@override
|
||||
ValueStream<List<SongModel>> get queueStream => _queueSubject.stream;
|
||||
|
||||
@override
|
||||
ValueStream<ShuffleMode> get shuffleModeStream => _shuffleModeSubject.stream;
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _audioPlayer.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> loadQueue(List<QueueItem> queue) {
|
||||
// TODO: implement loadQueue
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
await _audioPlayer.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
await _audioPlayer.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playSongList(List<SongModel> 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<void> seekToNext() async {
|
||||
await _audioPlayer.seekToNext();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seekToPrevious() async {
|
||||
await _audioPlayer.seekToPrevious();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setIndex(int index) async {
|
||||
await _audioPlayer.seek(const Duration(seconds: 0), index: index);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_audioPlayer.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> 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<void> removeQueueIndex(int index) async {
|
||||
_queue.removeAt(index);
|
||||
_queueSubject.add(_queue.map((e) => e.song).toList());
|
||||
await _audioSource.removeAt(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<List<MediaItem>> getMediaItemsFromPaths(List<String> paths) async {
|
||||
final mediaItems = <MediaItem>[];
|
||||
for (final path in paths) {
|
||||
final song = await _musicDataSource.getSongByPath(path);
|
||||
mediaItems.add(song.toMediaItem());
|
||||
}
|
||||
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
Future<List<QueueItem>> generateQueue(
|
||||
ShuffleMode shuffleMode,
|
||||
List<MediaItem> mediaItems,
|
||||
List<SongModel> songModels,
|
||||
int startIndex,
|
||||
) async {
|
||||
List<QueueItem> 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<MediaItem> 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<QueueItem> _generateNormalQueue(List<MediaItem> mediaItems) {
|
||||
ConcatenatingAudioSource songModelsToAudioSource(List<SongModel> songModels) {
|
||||
return ConcatenatingAudioSource(
|
||||
children: songModels.map((SongModel m) => AudioSource.uri(Uri.file(m.path))).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<QueueItem> _generateNormalQueue(List<SongModel> songs) {
|
||||
return List<QueueItem>.generate(
|
||||
mediaItems.length,
|
||||
songs.length,
|
||||
(i) => QueueItem(
|
||||
mediaItems[i],
|
||||
songs[i],
|
||||
originalIndex: i,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<QueueItem> _generateShuffleQueue(
|
||||
List<MediaItem> mediaItems,
|
||||
List<SongModel> songs,
|
||||
int startIndex,
|
||||
) {
|
||||
final List<QueueItem> queue = List<QueueItem>.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<List<QueueItem>> _generateShufflePlusQueue(
|
||||
List<MediaItem> mediaItems,
|
||||
List<SongModel> songs,
|
||||
int startIndex,
|
||||
) async {
|
||||
final List<QueueItem> queue = await _getQueueItemWithLinks(
|
||||
mediaItems[startIndex],
|
||||
songs[startIndex],
|
||||
startIndex,
|
||||
);
|
||||
final List<int> 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<List<QueueItem>> _getQueueItemWithLinks(
|
||||
MediaItem mediaItem,
|
||||
SongModel song,
|
||||
int index,
|
||||
) async {
|
||||
final List<QueueItem> 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<List<MediaItem>> _getPredecessors(MediaItem mediaItem) async {
|
||||
final List<MediaItem> mediaItems = [];
|
||||
MediaItem currentMediaItem = mediaItem;
|
||||
Future<List<SongModel>> _getPredecessors(SongModel song) async {
|
||||
final List<SongModel> 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<List<MediaItem>> _getSuccessors(MediaItem mediaItem) async {
|
||||
final List<MediaItem> mediaItems = [];
|
||||
MediaItem currentMediaItem = mediaItem;
|
||||
Future<List<SongModel>> _getSuccessors(SongModel song) async {
|
||||
final List<SongModel> 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();
|
||||
}
|
||||
}
|
||||
|
|
9
lib/system/audio/stream_constants.dart
Normal file
9
lib/system/audio/stream_constants.dart
Normal file
|
@ -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';
|
48
lib/system/models/player_state_model.dart
Normal file
48
lib/system/models/player_state_model.dart
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
Loading…
Add table
Reference in a new issue