2020-04-18 13:50:38 +02:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
2020-05-04 21:49:44 +02:00
|
|
|
import 'package:musicus_common/musicus_common.dart';
|
2020-04-21 17:37:01 +02:00
|
|
|
import 'package:musicus_player/musicus_player.dart';
|
2022-05-07 19:43:55 +02:00
|
|
|
import 'package:path/path.dart' as p;
|
2020-04-21 17:37:01 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
class MusicusMobilePlayback extends MusicusPlayback {
|
|
|
|
|
AudioHandler audioHandler;
|
|
|
|
|
MusicusLibrary library;
|
2020-04-21 19:50:18 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> setup(MusicusLibrary musicusLibrary) async {
|
|
|
|
|
library = musicusLibrary;
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
audioHandler = await AudioService.init(
|
|
|
|
|
builder: () => MusicusAudioHandler(musicusLibrary),
|
|
|
|
|
config: AudioServiceConfig(
|
|
|
|
|
androidNotificationChannelId: 'de.johrpan.musicus.channel.audio',
|
2020-04-18 13:50:38 +02:00
|
|
|
androidNotificationChannelName: 'Musicus playback',
|
|
|
|
|
androidNotificationChannelDescription:
|
|
|
|
|
'Keeps Musicus playing in the background',
|
|
|
|
|
androidNotificationIcon: 'drawable/ic_notification',
|
2022-05-07 19:43:55 +02:00
|
|
|
),
|
|
|
|
|
);
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
listen();
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> listen() async {
|
|
|
|
|
audioHandler.customEvent.listen((event) {
|
|
|
|
|
if (event != null && event is PlaylistEvent) {
|
|
|
|
|
playlist.add(event.playlist);
|
2020-05-03 22:45:28 +02:00
|
|
|
}
|
|
|
|
|
});
|
2020-05-04 09:23:49 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
audioHandler.playbackState.listen((event) {
|
|
|
|
|
if (event != null) {
|
|
|
|
|
playing.add(event.playing);
|
|
|
|
|
updatePosition(event.position);
|
|
|
|
|
updateCurrentTrack(event.queueIndex);
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-05-03 22:45:28 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
audioHandler.mediaItem.listen((event) {
|
|
|
|
|
if (event != null) {
|
|
|
|
|
updateDuration(event.duration);
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-04-21 19:50:18 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
await audioHandler.customAction('sendState');
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
|
2020-05-04 21:49:44 +02:00
|
|
|
@override
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> addTracks(List<String> tracks) async {
|
|
|
|
|
await audioHandler.customAction('addTracks', {'tracks': tracks});
|
|
|
|
|
active.add(true);
|
2020-04-21 17:37:01 +02:00
|
|
|
}
|
|
|
|
|
|
2020-05-04 21:49:44 +02:00
|
|
|
@override
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> playPause() async {
|
|
|
|
|
if (playing.value) {
|
|
|
|
|
await audioHandler.pause();
|
|
|
|
|
} else {
|
|
|
|
|
await audioHandler.play();
|
2020-04-26 18:54:49 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-04 21:49:44 +02:00
|
|
|
@override
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> removeTrack(int index) async {
|
|
|
|
|
await audioHandler.customAction('removeTrack', {'index': index});
|
2020-05-04 21:49:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
2020-04-18 13:50:38 +02:00
|
|
|
Future<void> seekTo(double pos) async {
|
2022-05-07 19:43:55 +02:00
|
|
|
if (pos >= 0.0 && pos <= 1.0) {
|
|
|
|
|
final durationMs = audioHandler.mediaItem.value.duration.inMilliseconds;
|
|
|
|
|
await audioHandler
|
|
|
|
|
.seek(Duration(milliseconds: (pos * durationMs).floor()));
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-04 21:49:44 +02:00
|
|
|
@override
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> skipTo(int index) async {
|
|
|
|
|
await audioHandler.skipToQueueItem(index);
|
2020-04-22 10:01:50 +02:00
|
|
|
}
|
|
|
|
|
|
2020-05-04 21:49:44 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> skipToNext() async {
|
2022-05-07 19:43:55 +02:00
|
|
|
await audioHandler.skipToNext();
|
2020-04-22 10:01:50 +02:00
|
|
|
}
|
|
|
|
|
|
2020-05-04 21:49:44 +02:00
|
|
|
@override
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> skipToPrevious() async {
|
|
|
|
|
await audioHandler.skipToPrevious();
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
class MusicusAudioHandler extends BaseAudioHandler {
|
|
|
|
|
final MusicusLibrary library;
|
2020-04-24 19:50:03 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
MusicusPlayer player;
|
2020-04-21 19:50:18 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
List<String> playlist = [];
|
|
|
|
|
int currentTrack = -1;
|
|
|
|
|
int durationMs = 1000;
|
|
|
|
|
bool playing = false;
|
2020-04-21 19:50:18 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
MusicusAudioHandler(this.library) {
|
|
|
|
|
player = MusicusPlayer(onComplete: () async {
|
|
|
|
|
if (currentTrack < playlist.length - 1) {
|
|
|
|
|
await skipToNext();
|
2020-04-24 19:50:03 +02:00
|
|
|
} else {
|
2022-05-07 19:43:55 +02:00
|
|
|
playing = false;
|
|
|
|
|
await sendState();
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
2020-04-21 17:37:01 +02:00
|
|
|
});
|
2020-05-04 09:23:49 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> play() async {
|
|
|
|
|
await player.play();
|
|
|
|
|
playing = true;
|
|
|
|
|
await sendState();
|
|
|
|
|
keepSendingPosition();
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> pause() async {
|
|
|
|
|
await player.pause();
|
|
|
|
|
playing = false;
|
|
|
|
|
await sendState();
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> stop() async {
|
|
|
|
|
playlist.clear();
|
|
|
|
|
await player.stop();
|
2020-04-21 19:50:18 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
super.stop();
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> seek(Duration position) async {
|
|
|
|
|
await player.seekTo(position.inMilliseconds);
|
|
|
|
|
await sendState();
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> skipToPrevious() async {
|
|
|
|
|
if (currentTrack > 0 && currentTrack < playlist.length) {
|
|
|
|
|
await skipToQueueItem(currentTrack - 1);
|
|
|
|
|
}
|
2020-04-21 19:50:18 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> skipToNext() async {
|
|
|
|
|
if (currentTrack >= 0 && currentTrack < playlist.length - 1) {
|
|
|
|
|
await skipToQueueItem(currentTrack + 1);
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> skipToQueueItem(int index) async {
|
|
|
|
|
if (index >= 0 && index < playlist.length) {
|
|
|
|
|
currentTrack = index;
|
|
|
|
|
final track = await library.db.tracksById(playlist[index]).getSingle();
|
|
|
|
|
durationMs = await player.setUri(p.join(library.basePath, track.path));
|
|
|
|
|
|
|
|
|
|
await sendState();
|
|
|
|
|
await sendMediaItem();
|
|
|
|
|
}
|
2020-04-24 19:50:03 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
@override
|
|
|
|
|
Future<void> customAction(String name, [Map<String, dynamic> extras]) async {
|
|
|
|
|
if (name == 'sendState') {
|
|
|
|
|
await sendPlaylist();
|
|
|
|
|
await sendMediaItem();
|
|
|
|
|
await sendState();
|
|
|
|
|
} else if (name == 'addTracks') {
|
|
|
|
|
await addTracks(extras['tracks']);
|
|
|
|
|
} else if (name == 'removeTrack') {
|
|
|
|
|
await removeTrack(extras['index']);
|
2020-04-21 19:50:18 +02:00
|
|
|
}
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> addTracks(List<String> tracks) async {
|
|
|
|
|
if (tracks != null && tracks.isNotEmpty) {
|
|
|
|
|
final wasEmpty = playlist.isEmpty;
|
2020-04-26 18:54:49 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
playlist.addAll(tracks);
|
|
|
|
|
await sendPlaylist();
|
2020-04-26 18:54:49 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
if (wasEmpty) {
|
|
|
|
|
await skipToQueueItem(0);
|
|
|
|
|
await play();
|
|
|
|
|
} else {
|
|
|
|
|
await sendState();
|
2020-04-26 18:54:49 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> removeTrack(int index) async {
|
|
|
|
|
if (index >= 0 && index < playlist.length) {
|
|
|
|
|
playlist.removeAt(index);
|
|
|
|
|
|
|
|
|
|
if (playlist.isNotEmpty) {
|
|
|
|
|
if (currentTrack == index) {
|
|
|
|
|
await skipToQueueItem(index);
|
|
|
|
|
} else if (currentTrack > index) {
|
|
|
|
|
currentTrack--;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await sendPlaylist();
|
|
|
|
|
await sendState();
|
2020-04-24 21:49:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> sendPlaylist() async {
|
|
|
|
|
customEvent.add(PlaylistEvent(playlist));
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> sendState() async {
|
|
|
|
|
List<MediaControl> controls = [];
|
|
|
|
|
Set<MediaAction> actions = {};
|
2020-04-21 17:37:01 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
if (playlist.isNotEmpty) {
|
|
|
|
|
if (currentTrack < 0 || currentTrack >= playlist.length) {
|
|
|
|
|
currentTrack = 0;
|
|
|
|
|
}
|
2020-04-24 19:50:03 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
if (currentTrack > 0) {
|
|
|
|
|
controls.add(MediaControl.skipToPrevious);
|
|
|
|
|
}
|
2020-04-21 17:37:01 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
if (playing) {
|
|
|
|
|
controls.add(MediaControl.pause);
|
|
|
|
|
} else {
|
|
|
|
|
controls.add(MediaControl.play);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentTrack < playlist.length - 1) {
|
|
|
|
|
controls.add(MediaControl.skipToNext);
|
|
|
|
|
}
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
actions.add(MediaAction.seek);
|
|
|
|
|
} else {
|
|
|
|
|
currentTrack = -1;
|
|
|
|
|
}
|
2020-04-24 19:50:03 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
playbackState.add(PlaybackState(
|
|
|
|
|
processingState: AudioProcessingState.ready,
|
|
|
|
|
playing: playing,
|
|
|
|
|
controls: controls,
|
|
|
|
|
systemActions: actions,
|
|
|
|
|
updatePosition: Duration(milliseconds: await player.getPosition()),
|
|
|
|
|
queueIndex: currentTrack,
|
|
|
|
|
));
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
Future<void> sendMediaItem() async {
|
|
|
|
|
if (currentTrack >= 0 && currentTrack < playlist.length) {
|
|
|
|
|
final track =
|
|
|
|
|
await library.db.tracksById(playlist[currentTrack]).getSingle();
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
final recording =
|
|
|
|
|
await library.db.recordingById(track.recording).getSingle();
|
2020-04-24 19:50:03 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
final workInfo = await library.db.getWork(recording.work);
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
final partIds = track.workParts
|
|
|
|
|
.split(',')
|
|
|
|
|
.where((p) => p.isNotEmpty)
|
|
|
|
|
.map((p) => int.parse(p))
|
|
|
|
|
.toList();
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
String title;
|
|
|
|
|
String subtitle;
|
2020-04-24 19:50:03 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
if (workInfo != null) {
|
|
|
|
|
title = '${workInfo.composer.firstName} ${workInfo.composer.lastName}';
|
2020-04-18 13:50:38 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
final subtitleBuffer = StringBuffer(workInfo.work.title);
|
2020-04-22 10:01:50 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
if (partIds.isNotEmpty) {
|
|
|
|
|
subtitleBuffer.write(': ');
|
|
|
|
|
subtitleBuffer
|
|
|
|
|
.write(partIds.map((i) => workInfo.parts[i].title).join(', '));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subtitle = subtitleBuffer.toString();
|
|
|
|
|
} else {
|
|
|
|
|
title = '...';
|
|
|
|
|
subtitle = '...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mediaItem.add(MediaItem(
|
|
|
|
|
id: track.id,
|
|
|
|
|
title: subtitle,
|
|
|
|
|
album: title,
|
|
|
|
|
duration: Duration(milliseconds: durationMs),
|
|
|
|
|
));
|
2020-04-22 10:01:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
/// Notify the UI of the new playback position periodically.
|
|
|
|
|
Future<void> keepSendingPosition() async {
|
|
|
|
|
while (playing) {
|
|
|
|
|
sendState();
|
|
|
|
|
await Future.delayed(const Duration(seconds: 1));
|
2020-04-22 10:01:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-05-07 19:43:55 +02:00
|
|
|
}
|
2020-04-22 10:01:50 +02:00
|
|
|
|
2022-05-07 19:43:55 +02:00
|
|
|
class PlaylistEvent {
|
|
|
|
|
final List<String> playlist;
|
|
|
|
|
PlaylistEvent(this.playlist);
|
2020-04-18 13:50:38 +02:00
|
|
|
}
|