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

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
    }
Last updated on 5 Sep 2022
Published on 5 Sep 2022