Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions example/events_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:mpris/mpris.dart';

void main() async {
final players = await MPRIS().getPlayers();
late final MPRISPlayer player;
for (final p in players) {
final id = await p.getIdentity();
if (id == 'Spotify') {
player = p;
}
}

player.propertiesChanged().listen((event) {
switch (event) {
case MetaDataChanged(:final metadata):
print(metadata);
case PlaybackStatusChanged(:final playbackStatus):
print(playbackStatus);
case LoopStatusChanged(:final loopStatus):
print(loopStatus);
case ShuffleChanged(:final shuffle):
print(shuffle);
case VolumeChanged(:final volume):
print(volume);
case UnsuportedEvent():
break;
}
});

player.seeked().listen(print);

MPRIS().playerChanged().listen((event) {
switch (event) {
case PlayerMountEvent(:final player):
player.toggle();
case PlayerUnmountEvent(:final playerName):
print(playerName);
case PlayerUnknownEvent(:final event):
print(event);
}
});
}
2 changes: 1 addition & 1 deletion example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ Future printPlaying(MPRISPlayer player) async {
// Some delay is required because changes don't happen instantly. The timout can also be lower, but then old metadata may be returned
await Future.delayed(const Duration(milliseconds: 500));
final metadata = await player.getMetadata();
print("Playing '${metadata.trackTitle}' by '${metadata.trackArtists[0]}'");
print("Playing '${metadata.trackTitle}' by '${metadata.trackArtists?[0]}'");
}
279 changes: 231 additions & 48 deletions lib/mpris.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,47 @@ class MPRIS {

/// Get a player from it's name
MPRISPlayer getPlayer(String name) => MPRISPlayer(_client, name);

/// player mount and unmount stream.
Stream<PlayerEvent> playerChanged() => _client.nameOwnerChanged
.where((e) => e.name.startsWith('org.mpris.MediaPlayer2'))
.map((e) => switch ((e.oldOwner, e.newOwner)) {
(null, String()) =>
PlayerMountEvent(MPRISPlayer(_client, e.name)),
(String(), null) => PlayerUnmountEvent(e.name),
(String(), String()) => PlayerUnknownEvent(e),
(null, null) => PlayerUnknownEvent(e),
});
}

// ignore: public_member_api_docs
sealed class PlayerEvent {
// ignore: public_member_api_docs
const PlayerEvent();
}

/// This class is used when a new player is mounted.
class PlayerMountEvent extends PlayerEvent {
// ignore: public_member_api_docs
const PlayerMountEvent(this.player);
// ignore: public_member_api_docs
final MPRISPlayer player;
}

/// Unknown state
class PlayerUnknownEvent extends PlayerEvent {
// ignore: public_member_api_docs
const PlayerUnknownEvent(this.event);
// ignore: public_member_api_docs
final DBusNameOwnerChangedEvent event;
}

/// This class is used when a new player is unmounted.
class PlayerUnmountEvent extends PlayerEvent {
// ignore: public_member_api_docs
const PlayerUnmountEvent(this.playerName);
// ignore: public_member_api_docs
final String playerName;
}

// ignore: public_member_api_docs
Expand All @@ -41,6 +82,32 @@ class MPRISPlayer {
final MediaPlayer2 _servicePlayer;
final MediaPlayer2Player _mediaPlayer;

/// Updates at each change of a property;
Stream<PropertyChangedEvent> propertiesChanged() =>
_mediaPlayer.propertiesChanged.map((event) {
final map = event.values[1].asStringVariantDict();

late final PropertyChangedEvent result;
if (map.containsKey('PlaybackStatus')) {
result = PlaybackStatusChanged(
PlaybackStatus.fromString(map['PlaybackStatus']!.asString()));
} else if (map.containsKey('Metadata')) {
result = MetaDataChanged(
Metadata.fromMap(map['Metadata']!.asStringVariantDict()));
} else if (map.containsKey('LoopStatus')) {
result = LoopStatusChanged(
LoopStatus.fromString(map['LoopStatus']!.asString()));
} else if (map.containsKey('Shuffle')) {
result = ShuffleChanged(map['Shuffle']!.asBoolean());
} else if (map.containsKey('Volume')) {
result = VolumeChanged(map['Volume']!.asDouble());
} else {
result = UnsuportedEvent(map);
}

return result;
});

/// A friendly name to identify the media player to users
Future<String> getIdentity() => _servicePlayer.getIdentity();

Expand Down Expand Up @@ -79,19 +146,17 @@ class MPRISPlayer {
/// Causes the media player to stop running
Future quit() => _servicePlayer.callQuit();

/// Get the status
/// Gets org.mpris.MediaPlayer2.Player.PlaybackStatus
Future<PlaybackStatus> getPlaybackStatus() async {
final status = await _mediaPlayer.getPlaybackStatus();
return PlaybackStatus.fromString(status);
}

/// Get the current loop / repeat status
Future<LoopStatus> getLoopStatus() async {
final status = await _mediaPlayer.getLoopStatus();
switch (status) {
case 'None':
return LoopStatus.none;
case 'Track':
return LoopStatus.track;
case 'Playlist':
return LoopStatus.playlist;
default:
throw Exception("Unknown loop status '$status'");
}
return LoopStatus.fromString(status);
}

/// Set the current loop / repeat status
Expand Down Expand Up @@ -191,11 +256,12 @@ class MPRISPlayer {
/// Uri of the track to load (This is used to tell the media player which track to play)
Future openUri(String uri) => _mediaPlayer.callOpenUri(uri);

/*
This doesn't work
Stream<MediaPlayer2PlayerSeeked> subscribeSeeked() =>
_mediaPlayer.subscribeSeeked();
*/
/// Indicates that the track position has changed in a way that is inconsistant with the current playing state.
/// When this signal is not received, clients should assume that:
/// - When playing, the position progresses according to the rate property.
/// - When paused, it remains constant.
Stream<Duration> seeked() => _mediaPlayer.seeked
.map((event) => Duration(microseconds: event.Position));
}

/// The current loop / repeat status
Expand All @@ -207,7 +273,45 @@ enum LoopStatus {
track,

/// If the playback loops through a list of tracks
playlist,
playlist;

factory LoopStatus.fromString(status) {
switch (status) {
case 'None':
return LoopStatus.none;
case 'Track':
return LoopStatus.track;
case 'Playlist':
return LoopStatus.playlist;
default:
throw Exception("Unknown loop status '$status'");
}
}
}

/// The current playback status
enum PlaybackStatus {
/// Playing normally
playing,

/// Paused
paused,

/// Stopped
stopped;

factory PlaybackStatus.fromString(String status) {
switch (status) {
case 'Playing':
return PlaybackStatus.playing;
case 'Paused':
return PlaybackStatus.paused;
case 'Stopped':
return PlaybackStatus.stopped;
default:
throw Exception("Unknown playback status '$status'");
}
}
}

// ignore: public_member_api_docs
Expand All @@ -228,57 +332,136 @@ class Metadata {

// ignore: public_member_api_docs
factory Metadata.fromMap(Map<String, DBusValue> map) => Metadata(
(map['mpris:trackid'] as DBusString).value,
(map['xesam:title'] as DBusString).value,
((map['xesam:artist'] as DBusArray).children)
.map((e) => (e as DBusString).value)
.toList(),
map['xesam:trackNumber'] is DBusInt32
? (map['xesam:trackNumber'] as DBusInt32).value
: (map['xesam:trackNumber'] as DBusUint32).value,
(map['xesam:url'] as DBusString).value,
Duration(
microseconds: map['mpris:length'] is DBusUint64
? (map['mpris:length'] as DBusUint64).value
: (map['mpris:length'] as DBusInt64).value,
),
(map['mpris:artUrl'] as DBusString).value,
(map['xesam:album'] as DBusString).value,
((map['xesam:albumArtist'] as DBusArray).children)
.map((e) => (e as DBusString).value)
.toList(),
map['xesam:discNumber'] is DBusInt32
? (map['xesam:discNumber'] as DBusInt32).value
: (map['xesam:discNumber'] as DBusUint32).value,
map['mpris:trackid'] != null
? (map['mpris:trackid'] as DBusString).value
: null,
map['xesam:title'] != null
? (map['xesam:title'] as DBusString).value
: null,
map['xesam:artist'] != null
? (map['xesam:artist'] as DBusArray)
.children
.map((e) => (e as DBusString).value)
.toList()
: null,
map['xesam:trackNumber'] != null
? (map['xesam:trackNumber'] is DBusInt32
? (map['xesam:trackNumber'] as DBusInt32).value
: (map['xesam:trackNumber'] as DBusUint32).value)
: null,
map['xesam:trackNumber'] != null
? (map['xesam:url'] as DBusString).value
: null,
map['mpris:length'] != null
? Duration(
microseconds: map['mpris:length'] is DBusUint64
? (map['mpris:length'] as DBusUint64).value
: (map['mpris:length'] as DBusInt64).value,
)
: null,
map['mpris:artUrl'] != null
? (map['mpris:artUrl'] as DBusString).value
: null,
map['xesam:album'] != null
? (map['xesam:album'] as DBusString).value
: null,
map['xesam:albumArtist'] != null
? (map['xesam:albumArtist'] as DBusArray)
.children
.map((e) => (e as DBusString).value)
.toList()
: null,
map['xesam:discNumber'] != null
? (map['xesam:discNumber'] is DBusInt32
? (map['xesam:discNumber'] as DBusInt32).value
: (map['xesam:discNumber'] as DBusUint32).value)
: null,
);

// ignore: public_member_api_docs
final String trackId;
final String? trackId;

// ignore: public_member_api_docs
final String? trackTitle;

// ignore: public_member_api_docs
final List<String>? trackArtists;

// ignore: public_member_api_docs
final String trackTitle;
final int? trackNumber;

// ignore: public_member_api_docs
final List<String> trackArtists;
final String? trackUrl;

// ignore: public_member_api_docs
final int trackNumber;
final Duration? trackLength;

// ignore: public_member_api_docs
final String trackUrl;
final String? trackArtUrl;

// ignore: public_member_api_docs
final Duration trackLength;
final String? albumName;

// ignore: public_member_api_docs
final String trackArtUrl;
final List<String>? albumArtists;

// ignore: public_member_api_docs
final String albumName;
final int? discNumber;
}

// The following code defines a set of events related to property changes.
// Each event is a subclass of the sealed class PropertyChangedEvent.

/// The base class for property changed events.
sealed class PropertyChangedEvent {
// ignore: public_member_api_docs
final List<String> albumArtists;
const PropertyChangedEvent();
}

/// Represents an unsupported event with a map of String keys and DBusValues.
class UnsuportedEvent extends PropertyChangedEvent {
// ignore: public_member_api_docs
const UnsuportedEvent(this.value);
// ignore: public_member_api_docs
final Map<String, DBusValue> value;
}

/// Represents a metadata change event with the updated metadata.
class MetaDataChanged extends PropertyChangedEvent {
// ignore: public_member_api_docs
const MetaDataChanged(this.metadata);
// ignore: public_member_api_docs
final Metadata metadata;
}

/// Represents a playback status change event with the new playback status.
class PlaybackStatusChanged extends PropertyChangedEvent {
// ignore: public_member_api_docs
const PlaybackStatusChanged(this.playbackStatus);
// ignore: public_member_api_docs
final PlaybackStatus playbackStatus;
}

/// Represents a loop status change event with the updated loop status.
class LoopStatusChanged extends PropertyChangedEvent {
// ignore: public_member_api_docs
const LoopStatusChanged(this.loopStatus);
// ignore: public_member_api_docs
final LoopStatus loopStatus;
}

/// Represents a shuffle change event with the new shuffle status.
class ShuffleChanged extends PropertyChangedEvent {
// ignore: public_member_api_docs
const ShuffleChanged(this.shuffle);
// ignore: public_member_api_docs
final bool shuffle;
}

/// Represents a volume change event with the updated volume level.
class VolumeChanged extends PropertyChangedEvent {
// ignore: public_member_api_docs
const VolumeChanged(this.volume);
// ignore: public_member_api_docs
final int discNumber;
final double volume;
}
Loading