D-Bus - Part 2
D-Bus
Before extending the code I wanted to understand what D-Bus was.
D-Bus is a message bus system, a simple way for applications to talk to one another.
D-Bus runs in 2 instances; session specific and system wide.
So, we do have some form of a message bus system which will allow this Rust program to consume information like:
-
Did the track change ?
-
Was the media paused ?
-
Did the media stop ?
-
What application was used to control the media ?
Rust already has a healthy number of libraries out there.
https://docs.rs/dbus/0.9.6/dbus/ was a good place to start to look at an existing Rust library.
use dbus::blocking::Connection;
let conn = Connection::new_session().expect("D-Bus connection failed");
This attempts to open a new connection to the session bus which is non-asynchronous.
D-Bus provides a wide range of ‘interfaces’, these are callable methods and signals which are published on the bus.
D-Bus interfaces are very helpful since it allows external programs access to consume messages on the bus.
MPRIS D-Bus Interface
MPRIS = Media Player Remote Interface Specification.
aims to provide a common programmatic API for controlling media players
It provides a mechanism for discovery, querying and basic playback control of compliant media players, as well as a tracklist interface which is used to add context to the active media item.
Each Bus name has an entry point, in this case it is ‘org.mpris.MediaPlayer2’.
Bus names take the form of “org.mpris.MediaPlayer2.$Player”.
Where $Player can take the name of a specific media player application .e.g. vlc, audacious, smplayer, spotify.
If you just use “org.mpris.MediaPlayer2.Player” you get access to a list of generic methods generally available to all media players.
Looking through the methods provided by “org.mpris.MediaPlayer2.Player”:
-
Next()
-
Previous()
-
Pause()
-
PlayPause()
-
Stop()
-
Play()
-
Seek(x: offset)
-
SetPosition(o: TrackId, x: Position)
-
OpenUri(s: Uri)
List of truncated Properties:
-
PlaybackStatus
-
Metadata
These 2 properties are used to capture the data we need to display inside a notification.
PlaybackStatus exposes a playback state (Playing, Paused, Stopped)
Metadata exposes media track properties such as (Artist, Genre, Title, AlbumArtist …)
The full specification for metadata is available from Metadata
Metadata
The following properties will be extracted and displayed inside the desktop notification.
Property | Description | Type |
---|---|---|
xesam:artist | Artist name | String Array |
xesam:title | Track title | String |
mpris:artUrl | Location of Album art image | URI |
xesam:album | Album name | String |
dbus-send
dbus-send - Send a message to a message bus.
If we were to query the D-Bus message via the command line.
Request:
dbus-send --print-reply --session \
--dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 \
org.freedesktop.DBus.Properties.Get \
string:'org.mpris.MediaPlayer2.Player' \
string:'Metadata'
Response:
method return time=1662465362.622039 sender=:1.53 -> destination=:1.272 serial=715 reply_serial=2
variant array [
dict entry(
string "mpris:trackid"
variant string "spotify:track:0jQV5A9bFTYfHdJW0LXW8l"
)
dict entry(
string "mpris:length"
variant uint64 207026000
)
dict entry(
string "mpris:artUrl"
variant string "https://open.spotify.com/image/ab67616d00001e024a7dcb87b8ec33f6c98ec5ff"
)
dict entry(
string "xesam:album"
variant string "35 Acoustic Country Hits 2019 (Instrumental)"
)
dict entry(
string "xesam:albumArtist"
variant array [
string "Guitar Tribute Players"
]
)
dict entry(
string "xesam:artist"
variant array [
string "Guitar Tribute Players"
]
)
dict entry(
string "xesam:autoRating"
variant double 0.15
)
dict entry(
string "xesam:discNumber"
variant int32 1
)
dict entry(
string "xesam:title"
variant string "Whiskey Glasses - Instrumental"
)
dict entry(
string "xesam:trackNumber"
variant int32 17
)
dict entry(
string "xesam:url"
variant string "https://open.spotify.com/track/0jQV5A9bFTYfHdJW0LXW8l"
)
]
So, now we need Rust to pull out these properties.
Proxy
Next, we try to create a Proxy
A D-Bus “Proxy” is a client-side object that corresponds to a remote object on the server side. Calling methods on the proxy object calls methods on the remote object.
More information can be found here
let mut rule = MatchRule::new();
let proxy = conn.with_proxy(
"org.mpris.MediaPlayer2.spotify",
"/org/mpris/MediaPlayer2",
time::Duration::from_millis(DBUS_PROXY_TIMEOUT),
);
let proxy_result: Result<(), dbus::Error> = proxy.method_call(
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
(vec![rule.match_str()], 0u32),
);
There are 3 things happening here.
First, define a match rule - this is a filter on which to match messages.
Second, use the D-Bus connection created earlier to create a Proxy to use the MPRIS interface.
Third, use the Proxy to call a interface “org.freedesktop.DBus.Properties”.
This interface exposes a method called “PropertiesChanged”.
MPRIS uses the “org.freedesktop.DBus.Properties.PropertiesChanged” signal to notify clients of changes in media player state.
https://specifications.freedesktop.org/mpris-spec/latest/#The-PropertiesChanged-signal
Extracting Metadata
The Metadata returned by D-Bus are contained inside a Map.
Here, we iterate the Map and check to see if each value matches the properties we are concerned about.
These values are extracted and inserted into our own HashMap<String><String>()
pub fn get_track_metadata_map<'a>(
iter: &mut Box<dyn Iterator<Item = &'a dyn RefArg> + 'a>,
) -> HashMap<&'a str, &'a dyn RefArg> {
let mut map = HashMap::new();
let arr: Vec<_> = iter.collect();
for i in (0..arr.len()).step_by(2) {
match arr[i].as_str() {
Some(val) => {
if val == "xesam:artist" {
map.insert("artist", arr[i + 1]);
} else if val == "xesam:title" {
map.insert("title", arr[i + 1]);
} else if val == "mpris:artUrl" {
map.insert("artUrl", arr[i + 1]);
} else if val == "xesam:album" {
map.insert("album", arr[i + 1]);
}
}
None => continue,
}
}
map
}