Rework communication with playback service

The playback service now has multiple message types.
This commit is contained in:
Elias Projahn 2020-04-24 19:50:03 +02:00
parent c5541bc391
commit d873465361

View file

@ -34,7 +34,7 @@ class Player {
final currentIndex = BehaviorSubject.seeded(0); final currentIndex = BehaviorSubject.seeded(0);
/// The currently played track. /// The currently played track.
/// ///
/// This will be null, if there is no current track. /// This will be null, if there is no current track.
final currentTrack = BehaviorSubject<InternalTrack>.seeded(null); final currentTrack = BehaviorSubject<InternalTrack>.seeded(null);
@ -57,12 +57,7 @@ class Player {
final normalizedPosition = BehaviorSubject.seeded(0.0); final normalizedPosition = BehaviorSubject.seeded(0.0);
StreamSubscription _playbackServiceStateSubscription; StreamSubscription _playbackServiceStateSubscription;
int _durationMs = 1000;
/// Update [position] and [normalizedPosition] from position in milliseconds.
void _updatePosition(int positionMs) {
position.add(Duration(milliseconds: positionMs));
normalizedPosition.add(positionMs / duration.value.inMilliseconds);
}
/// Set everything to its default because the playback service was stopped. /// Set everything to its default because the playback service was stopped.
void _stop() { void _stop() {
@ -90,6 +85,29 @@ class Player {
} }
} }
/// Update [position] and [normalizedPosition].
///
/// Requires [duration] to be up to date
void _updatePosition(int positionMs) {
position.add(Duration(milliseconds: positionMs));
normalizedPosition.add(positionMs / duration.value.inMilliseconds);
}
/// Update [position], [duration] and [normalizedPosition].
void _updateDuration(int positionMs, int durationMs) {
position.add(Duration(milliseconds: positionMs));
duration.add(Duration(milliseconds: durationMs));
normalizedPosition.add(positionMs / durationMs);
}
/// Update [currentIndex] and [currentTrack].
///
/// Requires [playlist] to be up to date.
void _updateCurrentTrack(int index) {
currentIndex.add(index);
currentTrack.add(playlist.value[index]);
}
/// Connect listeners and initialize streams. /// Connect listeners and initialize streams.
void setup() { void setup() {
if (_playbackServiceStateSubscription == null) { if (_playbackServiceStateSubscription == null) {
@ -101,16 +119,18 @@ class Player {
if (msg == null) { if (msg == null) {
_stop(); _stop();
} else { } else {
final state = msg as PlaybackServiceState; if (msg is _StatusMessage) {
playing.add(msg.playing);
// TODO: Consider checking, whether values have actually changed. } else if (msg is _PositionMessage) {
playlist.add(state.playlist); _updatePosition(msg.positionMs);
currentIndex.add(state.currentTrack); } else if (msg is _TrackMessage) {
currentTrack.add(state.playlist[state.currentTrack]); _updateCurrentTrack(msg.currentTrack);
playing.add(state.playing); _updateDuration(msg.positionMs, msg.durationMs);
position.add(Duration(milliseconds: state.positionMs)); } else if (msg is _PlaylistMessage) {
duration.add(Duration(milliseconds: state.durationMs)); playlist.add(msg.playlist);
normalizedPosition.add(state.positionMs / state.durationMs); _updateCurrentTrack(msg.currentTrack);
_updateDuration(msg.positionMs, msg.durationMs);
}
} }
}); });
IsolateNameServer.registerPortWithName(receivePort.sendPort, _portName); IsolateNameServer.registerPortWithName(receivePort.sendPort, _portName);
@ -159,7 +179,7 @@ class Player {
} }
/// Play the previous track in the playlist. /// Play the previous track in the playlist.
/// ///
/// If the player is not active or there is no previous track, this will do /// If the player is not active or there is no previous track, this will do
/// nothing. /// nothing.
Future<void> skipToNext() async { Future<void> skipToNext() async {
@ -169,7 +189,7 @@ class Player {
} }
/// Skip to the next track in the playlist. /// Skip to the next track in the playlist.
/// ///
/// If the player is not active or there is no next track, this will do /// If the player is not active or there is no next track, this will do
/// nothing. If more than five seconds of the current track have been played, /// nothing. If more than five seconds of the current track have been played,
/// this will go back to its beginning instead. /// this will go back to its beginning instead.
@ -200,49 +220,75 @@ class Player {
} }
} }
/// Bundle of the current state of the playback service. /// A message from the playback service to the UI.
class PlaybackServiceState { abstract class _Message {}
/// The current playlist.
final List<InternalTrack> playlist;
/// The index of the currentTrack.
final int currentTrack;
/// Playback status update.
class _StatusMessage extends _Message {
/// Whether the player is playing (or paused). /// Whether the player is playing (or paused).
final bool playing; final bool playing;
/// The current playback position in milliseconds. _StatusMessage({
this.playing,
});
}
/// The playback position has changed.
///
/// This could be due to seeking or because time progressed.
class _PositionMessage extends _Message {
/// Playback position in milliseconds.
final int positionMs; final int positionMs;
/// The duration of the currently played track in milliseconds. _PositionMessage({
this.positionMs,
});
}
/// The current track has changed.
///
/// This also notifies about the playback position, as the old position could be
/// behind the new duration.
class _TrackMessage extends _Message {
/// Index of the new track within the playlist.
final int currentTrack;
/// Duration of the new track in milliseconds.
final int durationMs; final int durationMs;
PlaybackServiceState({ /// Playback position in milliseconds.
final int positionMs;
_TrackMessage({
this.currentTrack,
this.durationMs,
this.positionMs,
});
}
/// The playlist was changed.
///
/// This also notifies about the current track, as the old index could be out of
/// range in the new playlist.
class _PlaylistMessage extends _Message {
/// The new playlist.
final List<InternalTrack> playlist;
/// The current track.
final int currentTrack;
/// Duration of the current track in milliseconds.
final int durationMs;
/// Playback position in milliseconds.
final int positionMs;
_PlaylistMessage({
this.playlist, this.playlist,
this.currentTrack, this.currentTrack,
this.playing,
this.positionMs,
this.durationMs, this.durationMs,
this.positionMs,
}); });
factory PlaybackServiceState.fromJson(Map<String, dynamic> json) =>
PlaybackServiceState(
playlist: json['playlist']
.map<InternalTrack>((j) => InternalTrack.fromJson(j))
.toList(),
currentTrack: json['currentTrack'],
playing: json['playing'],
positionMs: json['positionMs'],
durationMs: json['durationMs'],
);
Map<String, dynamic> toJson() => {
'playlist': playlist.map((t) => t.toJson()),
'currentTrack': currentTrack,
'playing': playing,
'positionMs': positionMs,
'durationMs': durationMs,
};
} }
class _PlaybackService extends BackgroundAudioTask { class _PlaybackService extends BackgroundAudioTask {
@ -283,16 +329,19 @@ class _PlaybackService extends BackgroundAudioTask {
int _durationMs = 1000; int _durationMs = 1000;
_PlaybackService() { _PlaybackService() {
_player = MusicusPlayer(onComplete: () { _player = MusicusPlayer(onComplete: () async {
// TODO: Go to next track. if (_currentTrack < _playlist.length - 1) {
await _setCurrentTrack(_currentTrack + 1);
_sendTrack();
} else {
_playing = false;
_sendStatus();
_setState();
}
}); });
} }
Future<void> _sendMsg(dynamic msg) { /// Update the audio service status for the system.
final sendPort = IsolateNameServer.lookupPortByName(_portName);
sendPort?.send(msg);
}
Future<void> _setState() async { Future<void> _setState() async {
final positionMs = await _player.getPosition() ?? 0; final positionMs = await _player.getPosition() ?? 0;
final updateTime = DateTime.now().millisecondsSinceEpoch; final updateTime = DateTime.now().millisecondsSinceEpoch;
@ -307,27 +356,75 @@ class _PlaybackService extends BackgroundAudioTask {
); );
AudioServiceBackground.setMediaItem(dummyMediaItem); AudioServiceBackground.setMediaItem(dummyMediaItem);
}
_sendMsg(PlaybackServiceState( /// Send a message to the UI.
playlist: _playlist, void _sendMsg(_Message msg) {
currentTrack: _currentTrack, final sendPort = IsolateNameServer.lookupPortByName(_portName);
sendPort?.send(msg);
}
/// Notify the UI about the current playback status.
void _sendStatus() {
_sendMsg(_StatusMessage(
playing: _playing, playing: _playing,
positionMs: positionMs,
durationMs: _durationMs,
)); ));
} }
/// Notify the UI about the current playback position.
Future<void> _sendPosition() async {
_sendMsg(_PositionMessage(
positionMs: await _player.getPosition(),
));
}
/// Notify the UI about the current track.
Future<void> _sendTrack() async {
_sendMsg(_TrackMessage(
currentTrack: _currentTrack,
durationMs: _durationMs,
positionMs: await _player.getPosition(),
));
}
/// Notify the UI about the current playlist.
Future<void> _sendPlaylist() async {
_sendMsg(_PlaylistMessage(
playlist: _playlist,
currentTrack: _currentTrack,
durationMs: _durationMs,
positionMs: await _player.getPosition(),
));
}
/// Notify the UI of the new playback position periodically.
Future<void> _updatePosition() async { Future<void> _updatePosition() async {
while (_playing) { while (_playing) {
await Future.delayed( await Future.delayed(
const Duration(milliseconds: positionUpdateInterval)); const Duration(milliseconds: positionUpdateInterval));
_sendPosition();
// TODO: Consider seperating position updates from general state updates
// and/or estimating the position instead of asking the player.
_setState();
} }
} }
/// Set the current track, update the player and notify the UI.
Future<void> _setCurrentTrack(int index) async {
_currentTrack = index;
_durationMs = await _player.setUri(_playlist[_currentTrack].uri);
_setState();
}
/// Add [tracks] to the playlist.
Future<void> _addTracks(List<InternalTrack> tracks) async {
final play = _playlist.isEmpty;
_playlist.addAll(tracks);
if (play) {
await _setCurrentTrack(0);
}
_sendPlaylist();
}
@override @override
Future<void> onStart() async { Future<void> onStart() async {
_setState(); _setState();
@ -344,23 +441,19 @@ class _PlaybackService extends BackgroundAudioTask {
final tracksJson = jsonDecode(arguments); final tracksJson = jsonDecode(arguments);
final List<InternalTrack> tracks = List.castFrom( final List<InternalTrack> tracks = List.castFrom(
tracksJson.map((j) => InternalTrack.fromJson(j)).toList()); tracksJson.map((j) => InternalTrack.fromJson(j)).toList());
_playlist.addAll(tracks);
_player.setUri(tracks.first.uri).then((newDurationMs) { _addTracks(tracks);
_durationMs = newDurationMs;
_setState();
});
} }
if (name == 'skipTo') { if (name == 'skipTo') {
final index = arguments as int; final index = arguments as int;
if (index >= 0 && index < _playlist.length) { if (index >= 0 && index < _playlist.length) {
_currentTrack = index; _setCurrentTrack(index);
_player.setUri(_playlist[index].uri); _sendTrack();
_setState();
} }
} else if (name == 'sendState') { } else if (name == 'sendState') {
// Send the current state to the main isolate. _sendPlaylist();
_setState(); _sendStatus();
} }
} }
@ -370,6 +463,8 @@ class _PlaybackService extends BackgroundAudioTask {
_player.play(); _player.play();
_playing = true; _playing = true;
_sendStatus();
_updatePosition(); _updatePosition();
_setState(); _setState();
} }
@ -380,42 +475,43 @@ class _PlaybackService extends BackgroundAudioTask {
_player.pause(); _player.pause();
_playing = false; _playing = false;
_sendStatus();
_setState(); _setState();
} }
@override @override
void onSeekTo(int position) { Future<void> onSeekTo(int position) async {
super.onSeekTo(position); super.onSeekTo(position);
_player.seekTo(position).then((_) { await _player.seekTo(position);
_setState();
}); _sendPosition();
_setState();
} }
@override @override
void onSkipToNext() { Future<void> onSkipToNext() async {
super.onSkipToNext(); super.onSkipToNext();
if (_playlist.length > 1 && _currentTrack < _playlist.length - 1) { if (_playlist.length > 1 && _currentTrack < _playlist.length - 1) {
_currentTrack++; await _setCurrentTrack(_currentTrack + 1);
_player.setUri(_playlist[_currentTrack].uri); _sendTrack();
_setState();
} }
} }
@override @override
void onSkipToPrevious() async { Future<void> onSkipToPrevious() async {
super.onSkipToPrevious(); super.onSkipToPrevious();
// If more than five seconds of the current track have been played, go back // If more than five seconds of the current track have been played, go back
// to its beginning, else, switch to the previous track. // to its beginning, else, switch to the previous track.
if (await _player.getPosition() > 5000) { if (await _player.getPosition() > 5000) {
await _player.setUri(_playlist[_currentTrack].uri); await _setCurrentTrack(_currentTrack);
_setState(); _sendTrack();
} else if (_playlist.length > 1 && _currentTrack > 0) { } else if (_playlist.length > 1 && _currentTrack > 0) {
_currentTrack--; await _setCurrentTrack(_currentTrack - 1);
_player.setUri(_playlist[_currentTrack].uri); _sendTrack();
_setState();
} }
} }