queue links implemented; cleanup

This commit is contained in:
Moritz Weber 2020-10-21 20:58:55 +02:00
parent cbb8559954
commit a031fedd39
13 changed files with 306 additions and 105 deletions

View file

@ -10,8 +10,8 @@ import 'domain/repositories/music_data_repository.dart';
import 'presentation/state/audio_store.dart';
import 'presentation/state/music_data_store.dart';
import 'presentation/state/navigation_store.dart';
import 'system/datasources/audio_manager.dart';
import 'system/datasources/audio_manager_contract.dart';
import 'system/audio/audio_manager.dart';
import 'system/audio/audio_manager_contract.dart';
import 'system/datasources/local_music_fetcher.dart';
import 'system/datasources/local_music_fetcher_contract.dart';
import 'system/datasources/moor_music_data_source.dart';

View file

@ -1,7 +1,7 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import '../../system/datasources/audio_player_task.dart';
import '../../system/audio/audio_player_task.dart';
class AudioServiceWidget extends StatefulWidget {
const AudioServiceWidget({@required this.child});

View file

@ -4,13 +4,15 @@ import 'dart:ui';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart';
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart';
import '../../domain/entities/shuffle_mode.dart';
import '../datasources/moor_music_data_source.dart';
import '../models/queue_item.dart';
import '../models/song_model.dart';
import 'moor_music_data_source.dart';
import 'queue_manager.dart';
import 'queue_generator.dart';
const String INIT = 'INIT';
const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT';
@ -22,13 +24,15 @@ const String KEY_INDEX = 'INDEX';
class AudioPlayerTask extends BackgroundAudioTask {
final audioPlayer = AudioPlayer();
MoorMusicDataSource moorMusicDataSource;
QueueManager qm;
QueueGenerator queueGenerator;
// TODO: confusing naming
List<MediaItem> originalPlaybackContext = <MediaItem>[];
List<MediaItem> playbackContext = <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<int> permutation;
ShuffleMode _shuffleMode = ShuffleMode.none;
ShuffleMode get shuffleMode => _shuffleMode;
@ -40,10 +44,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
int _playbackIndex = -1;
int get playbackIndex => _playbackIndex;
set playbackIndex(int i) {
print(i);
if (i != null) {
_playbackIndex = i;
AudioServiceBackground.setMediaItem(playbackContext[i]);
AudioServiceBackground.setMediaItem(playbackContext[i].mediaItem);
AudioServiceBackground.sendCustomEvent({KEY_INDEX: i});
AudioServiceBackground.setState(
@ -61,6 +64,12 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
}
static final _log = Logger('AudioPlayerTask')
..onRecord.listen((record) {
print(
'${record.time} [${record.level.name}] ${record.loggerName}: ${record.message}');
});
@override
Future<void> onStop() async {
await audioPlayer.stop();
@ -112,19 +121,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
Future<void> init() async {
print('AudioPlayerTask.init');
audioPlayer.playerStateStream.listen((event) => handlePlayerState(event));
audioPlayer.currentIndexStream.listen((event) => playbackIndex = event);
audioPlayer.sequenceStateStream
.listen((event) => playbackIndex = event?.currentIndex);
.listen((event) => handleSequenceState(event));
final connectPort = IsolateNameServer.lookupPortByName(MOOR_ISOLATE);
final MoorIsolate moorIsolate = MoorIsolate.fromConnectPort(connectPort);
final DatabaseConnection databaseConnection = await moorIsolate.connect();
moorMusicDataSource = MoorMusicDataSource.connect(databaseConnection);
qm = QueueManager(moorMusicDataSource);
queueGenerator = QueueGenerator(moorMusicDataSource);
}
Future<void> playWithContext(List<String> context, int index) async {
final mediaItems = await qm.getMediaItemsFromPaths(context);
final mediaItems = await queueGenerator.getMediaItemsFromPaths(context);
playPlaylist(mediaItems, index);
}
@ -137,22 +147,44 @@ class AudioPlayerTask extends BackgroundAudioTask {
Future<void> setShuffleMode(ShuffleMode mode) async {
shuffleMode = mode;
final index = permutation[playbackIndex];
permutation =
qm.generatePermutation(shuffleMode, originalPlaybackContext, index);
final QueueItem currentQueueItem = playbackContext[playbackIndex];
final int index = currentQueueItem.originalIndex;
playbackContext =
qm.getPermutatedSongs(originalPlaybackContext, permutation);
await queueGenerator.generateQueue(shuffleMode, originalPlaybackContext, index);
final queuedMediaItems = playbackContext.map((e) => e.mediaItem).toList();
AudioServiceBackground.setQueue(playbackContext);
// FIXME: this does not react correctly when inserted track is currently played
AudioServiceBackground.setQueue(queuedMediaItems);
final newQueue = queueGenerator.mediaItemsToAudioSource(queuedMediaItems);
_updateQueue(newQueue, currentQueueItem);
}
void _updateQueue(ConcatenatingAudioSource newQueue, QueueItem currentQueueItem) {
final int index = currentQueueItem.originalIndex;
final newQueue = qm.mediaItemsToAudioSource(playbackContext);
queue.removeRange(0, playbackIndex);
queue.removeRange(1, queue.length);
if (shuffleMode == ShuffleMode.none) {
queue.insertAll(0, newQueue.children.sublist(0, index));
queue.addAll(newQueue.children.sublist(index + 1));
playbackIndex = index;
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));
}
@ -171,18 +203,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
Future<void> playPlaylist(List<MediaItem> mediaItems, int index) async {
permutation = qm.generatePermutation(shuffleMode, mediaItems, index);
playbackContext = qm.getPermutatedSongs(mediaItems, permutation);
originalPlaybackContext = mediaItems;
AudioServiceBackground.setQueue(playbackContext);
queue = qm.mediaItemsToAudioSource(playbackContext);
playbackContext = await queueGenerator.generateQueue(shuffleMode, mediaItems, index);
final queuedMediaItems = playbackContext.map((e) => e.mediaItem).toList();
AudioServiceBackground.setQueue(queuedMediaItems);
queue = queueGenerator.mediaItemsToAudioSource(queuedMediaItems);
audioPlayer.play();
final int startIndex = shuffleMode == ShuffleMode.none ? index : 0;
await audioPlayer.load(queue, initialIndex: startIndex);
}
void handlePlayerState(PlayerState ps) {
_log.info('handlePlayerState called');
if (ps.processingState == ProcessingState.ready && ps.playing) {
AudioServiceBackground.setState(
controls: [
@ -211,4 +245,14 @@ class AudioPlayerTask extends BackgroundAudioTask {
);
}
}
// 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');
AudioServiceBackground.setMediaItem(
playbackContext[playbackIndex].mediaItem);
}
}
}

View file

@ -0,0 +1,176 @@
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import '../../domain/entities/shuffle_mode.dart';
import '../datasources/music_data_source_contract.dart';
import '../models/queue_item.dart';
import '../models/song_model.dart';
class QueueGenerator {
QueueGenerator(this._musicDataSource);
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,
int startIndex,
) async {
List<QueueItem> queue;
switch (shuffleMode) {
case ShuffleMode.none:
queue = _generateNormalQueue(mediaItems);
break;
case ShuffleMode.standard:
queue = _generateShuffleQueue(mediaItems, startIndex);
break;
case ShuffleMode.plus:
queue = await _generateShufflePlusQueue(mediaItems, startIndex);
}
return queue;
}
ConcatenatingAudioSource mediaItemsToAudioSource(List<MediaItem> mediaItems) {
return ConcatenatingAudioSource(
children: mediaItems
.map((MediaItem m) => AudioSource.uri(Uri.file(m.id)))
.toList());
}
List<QueueItem> _generateNormalQueue(List<MediaItem> mediaItems) {
return List<QueueItem>.generate(
mediaItems.length,
(i) => QueueItem(
mediaItems[i],
originalIndex: i,
),
);
}
List<QueueItem> _generateShuffleQueue(
List<MediaItem> mediaItems,
int startIndex,
) {
final List<QueueItem> queue = List<QueueItem>.generate(
mediaItems.length,
(i) => QueueItem(
mediaItems[i],
originalIndex: i,
),
);
queue.removeAt(startIndex);
queue.shuffle();
final first = QueueItem(
mediaItems[startIndex],
originalIndex: startIndex,
);
return [first] + queue;
}
Future<List<QueueItem>> _generateShufflePlusQueue(
List<MediaItem> mediaItems,
int startIndex,
) async {
final List<QueueItem> queue = await _getQueueItemWithLinks(
mediaItems[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)) {
indices.add(i);
}
}
indices.shuffle();
for (var i = 0; i < indices.length; i++) {
final int index = indices[i];
final MediaItem mediaItem = mediaItems[index];
queue.addAll(await _getQueueItemWithLinks(mediaItem, index));
}
return queue;
}
// TODO: naming things is hard
Future<List<QueueItem>> _getQueueItemWithLinks(
MediaItem mediaItem,
int index,
) async {
final List<QueueItem> queueItems = [];
final predecessors = await _getPredecessors(mediaItem);
final successors = await _getSuccessors(mediaItem);
for (final p in predecessors) {
queueItems.add(QueueItem(
p,
originalIndex: index,
type: QueueItemType.predecessor,
));
}
queueItems.add(QueueItem(
mediaItem,
originalIndex: index,
));
for (final p in successors) {
queueItems.add(QueueItem(
p,
originalIndex: index,
type: QueueItemType.successor,
));
}
return queueItems;
}
Future<List<MediaItem>> _getPredecessors(MediaItem mediaItem) async {
final List<MediaItem> mediaItems = [];
MediaItem currentMediaItem = mediaItem;
while (currentMediaItem.previous != null) {
currentMediaItem =
(await _musicDataSource.getSongByPath(currentMediaItem.previous))
.toMediaItem();
mediaItems.add(currentMediaItem);
}
return mediaItems.reversed.toList();
}
Future<List<MediaItem>> _getSuccessors(MediaItem mediaItem) async {
final List<MediaItem> mediaItems = [];
MediaItem currentMediaItem = mediaItem;
while (currentMediaItem.next != null) {
currentMediaItem =
(await _musicDataSource.getSongByPath(currentMediaItem.next))
.toMediaItem();
mediaItems.add(currentMediaItem);
}
return mediaItems.toList();
}
}

View file

@ -1,73 +0,0 @@
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import '../../domain/entities/shuffle_mode.dart';
import 'music_data_source_contract.dart';
class QueueManager {
QueueManager(this._musicDataSource);
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;
}
// TODO: test
List<int> generatePermutation(
ShuffleMode shuffleMode, List<MediaItem> mediaItems, int startIndex) {
// permutation[i] = j; => song j is on the i-th position in the permutated list
List<int> permutation;
final int length = mediaItems.length;
switch (shuffleMode) {
case ShuffleMode.none:
permutation = List<int>.generate(length, (i) => i);
break;
case ShuffleMode.standard:
final tmp = List<int>.generate(length, (i) => i)
..removeAt(startIndex)
..shuffle();
permutation = [startIndex] + tmp;
break;
case ShuffleMode.plus:
permutation = generatePlusPermutation(mediaItems, startIndex);
}
return permutation;
}
List<MediaItem> getPermutatedSongs(
List<MediaItem> songs, List<int> permutation) {
return List.generate(
permutation.length, (index) => songs[permutation[index]]);
}
ConcatenatingAudioSource mediaItemsToAudioSource(List<MediaItem> mediaItems) {
return ConcatenatingAudioSource(
children: mediaItems
.map((MediaItem m) => AudioSource.uri(Uri.file(m.id)))
.toList());
}
List<int> generatePlusPermutation(
List<MediaItem> mediaItems, int startIndex) {
final List<int> indices = [];
for (var i = 0; i < mediaItems.length; i++) {
if (i != startIndex && mediaItems[i].extras['blocked'] == 'false') {
indices.add(i);
}
}
indices.shuffle();
return [startIndex] + indices;
}
}

View file

@ -0,0 +1,11 @@
import 'package:audio_service/audio_service.dart';
class QueueItem {
QueueItem(this.mediaItem, {this.originalIndex, this.type = QueueItemType.standard});
final MediaItem mediaItem;
final int originalIndex;
final QueueItemType type;
}
enum QueueItemType { standard, predecessor, successor }

View file

@ -209,3 +209,9 @@ class SongModel extends Song {
return [discNumber, trackNumber];
}
}
// TODO: maybe move to another file
extension SongModelExtension on MediaItem {
String get previous => extras['previous'] as String;
String get next => extras['next'] as String;
}

View file

@ -5,7 +5,7 @@ import '../../domain/entities/playback_state.dart';
import '../../domain/entities/shuffle_mode.dart';
import '../../domain/entities/song.dart';
import '../../domain/repositories/audio_repository.dart';
import '../datasources/audio_manager_contract.dart';
import '../audio/audio_manager_contract.dart';
import '../models/song_model.dart';
class AudioRepositoryImpl implements AudioRepository {

View file

@ -3,13 +3,13 @@
import 'package:mucke/injection_container.dart';
import 'package:mucke/system/repositories/audio_repository_impl.dart';
import 'package:mucke/system/repositories/music_data_repository_impl.dart';
import 'package:mucke/system/datasources/audio_manager_contract.dart';
import 'package:mucke/system/audio/audio_manager_contract.dart';
import 'package:mucke/system/datasources/local_music_fetcher.dart';
import 'package:mucke/system/datasources/audio_player_task.dart';
import 'package:mucke/system/audio/audio_player_task.dart';
import 'package:mucke/system/datasources/local_music_fetcher_contract.dart';
import 'package:mucke/system/datasources/audio_manager.dart';
import 'package:mucke/system/audio/audio_manager.dart';
import 'package:mucke/system/datasources/moor_music_data_source.dart';
import 'package:mucke/system/datasources/queue_manager.dart';
import 'package:mucke/system/audio/queue_generator.dart';
import 'package:mucke/system/datasources/music_data_source_contract.dart';
import 'package:mucke/system/models/artist_model.dart';
import 'package:mucke/system/models/album_model.dart';

View file

@ -0,0 +1,37 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mucke/system/datasources/music_data_source_contract.dart';
import 'package:mucke/system/audio/queue_generator.dart';
import 'package:mucke/system/models/album_model.dart';
import 'package:mucke/system/models/song_model.dart';
import '../../test_constants.dart';
class MockMusicDataSource extends Mock implements MusicDataSource {}
void main() {
QueueGenerator queueManager;
MusicDataSource musicDataSource;
List<MediaItem> mediaItems;
group('generatePlusPermutation', () {
setUp(() {
musicDataSource = MockMusicDataSource();
queueManager = QueueGenerator(musicDataSource);
});
test(
'should exclude blocked songs',
() async {
// arrange
// act
// assert
},
);
});
}

View file

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mucke/system/datasources/audio_manager_contract.dart';
import 'package:mucke/system/audio/audio_manager_contract.dart';
import 'package:mucke/system/models/song_model.dart';
import 'package:mucke/system/repositories/audio_repository_impl.dart';