Introduction

I’ve always been annoyed that the music I listen to when I work continues to play when I’m away from the keyboard. Having to listen to the end of your favorite Hans Zimmer soundtrack when coming back from a break is not fun.

Hopefully there is a thing I always do when I’m leaving my computer: lock the screen. So I started looking around on the Internet, thinking “There must be someone who did this before”. But no, I had leads, but not a functional and plug and play solution I could just install on my system.

I also needed it to work with pretty much anything that could play audio, because of a second problem : my headphones are pretty noisy when not on my head, so if I forget a Youtube video playing for example, it could annoy some of my coworkers.

Building the solution

Listening to screen locks/unlocks

My first step was to find a way to know when I’m locking my computer, programmatically. I’m using Cinnamon’s (even though I’m not using Cinnamon, but they managed to make it modular enough!). This lock screen is based on Gnome’s that is pretty widely used, so I was able to quickly find a way to listen to the events through dbus on StackExchange’s UNIX forum.

I just had to replace the interface with my screenlockers (org.cinnamon.ScreenSaver), and voilà:

dbus-monitor --session "type='signal',interface='org.cinnamon.ScreenSaver'" | \
( while true
    do read X
    if echo $X | grep "boolean true" &> /dev/null; then
      echo "pause"
    elif echo $X | grep "boolean false" &> /dev/null; then
      echo "play"
    fi
    done )

This nifty little script (let’s call it listen-lock.sh) allows us to react on screen locks/unlocks.

Pausing and playing through MPRIS2

So we’re looking to programatically pause and play (control) as many audio players as we can, through ideally a single interface. A quick search leads us to MPRIS, whose name actually really describes what we need: Media Player Remote Interfacing Specification. Great!

Now, not every player out there supports this spec, but a reasonable amount do. At least the one I’m using the most, Spotify, implements the spec! If your player of choice doesn’t, it’s possible to install plugins. Here are the ones I’m using:

For debugging purposes, I’m using Qt’s D-Bus Viewer. It allows to search for “mpris” and validate that your plugins are working. For example:

Keyboard > Layouts > Options... window

I can validate both my mpv plugin and Spotify’s native interface work as expected.

You can now send messages to the MPRIS2 interface with the dbus-send command. The verbs we’re interested in are:

  • org.mpris.MediaPlayer2.Player.Pause for pausing
  • org.mpris.MediaPlayer2.Player.Play for playing

Let’s try pausing:

dbus-send --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause

If that works, congrats! Now let’s put all that together to have a first version of our script.

Putting it together

We can use our first script (listen-lock.sh) as an event emitter and react on these events to play or pause a chosen player via MPRIS.

#!/bin/bash

./listen-lock.sh | \
( while true
    do read X
    if echo $X | grep "pause" &> /dev/null; then
        dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause
    elif echo $X | grep "play" &> /dev/null; then
        dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play
    fi
    done )

This first version should pause on lock, and play Spotify on unlock!

Only resume if previously playing

Now that you’ve used the previous version for a bit, you quickly noticed a big problem. Your player is paused, you lock, and suprise music comes out when you unlock! One way to fix that is to keep a state of “playing” players when locking, so we can correctly restore that state on unlock.

To know if a player is currently playing, we can once again use D-BUS with the MPRIS2 interface:

dbus-send \
    --print-reply \
    --dest=org.mpris.MediaPlayer2.spotify \
    /org/mpris/MediaPlayer2 \
    org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus

This will output something like:

method return time=1556097097.642530 sender=:1.406 -> destination=:1.8488 serial=3827 reply_serial=2
   variant       string "Playing"

We don’t care about the first line, but the second should tell us wether the player is playing or not. If the player is paused, the string will be "Paused". If the MPRIS interface isn’t available (i.e. the player isn’t launched), the command will exit with the error code 1.

Given these information, we can easily get a status with the following command:

status=$((
    dbus-send \
    --print-reply \
    --dest=org.mpris.MediaPlayer2.spotify \
    /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus' \
    2>/dev/null | tail -1 | cut -d\" -f2
) || echo "")

This way, the status variable will either be "Playing", "Paused" or "" when no status is available.

Caveat: If you are playing multiple audio in one medium, e.g. Chrome or MPV, this script only handles one “channel” per medium. It would be pretty straightforward to add a looping mechanism to handle these.

Let’s put all this together:

#!/bin/bash

last_playing=""

./listen-lock.sh | \
( while true
    do read X
    if echo $X | grep "pause" &> /dev/null; then
        last_playing=""
        status=$((
            dbus-send \
                --print-reply \
                --dest=org.mpris.MediaPlayer2.spotify \
                /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus' \
                2>/dev/null | tail -1 | cut -d\" -f2
        ) || echo "")
        if [ "$status" = "Playing" ]; then
            last_playing="spotify"
            dbus-send --print-reply --dest="org.mpris.MediaPlayer2.$last_playing" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause
        fi
    elif echo $X | grep "play" &> /dev/null; then
      dbus-send --print-reply --dest="org.mpris.MediaPlayer2.$last_playing" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play
    fi
done )

What about multiple players support you say?

Multiple players

Just add a for loop per action and send the D-Bus messages for each command, and transform our last_playing variable to an array!

#!/bin/bash

players=("spotify" "chrome" "mpv")
last_playing=()

./listen-lock.sh | \
( while true
    do read X
    if echo $X | grep "pause" &> /dev/null; then
        last_playing=()
        for player in "${players[@]}"; do
            status=$((
                dbus-send \
                --print-reply \
                --dest="org.mpris.MediaPlayer2.$player" \
                /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus' \
                2>/dev/null | tail -1 | cut -d\" -f2
            ) || echo "")
        done
    elif echo $X | grep "play" &> /dev/null; then
        for player in "${last_playing[@]}"; do
            dbus-send --print-reply --dest="org.mpris.MediaPlayer2.$player" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play
        done
    fi
done )

Bonus: stopping music playback on jack connect/disconnect

Additionnally to screen lock/unlock, I wanted my music to stop when I was unplugging my headphones, when going to a meeting with my laptop for example. Given our current script and implementation, it’s very easy to add new ways to trigger the pause/unpause of the media players.

After a bit of Google-fu we quickly find that a way to listen to these events is provided through the acpi_listen command, of the acpid daemon.

acpi_listen will produce lines similar to "jack/headphone HEADPHONE unplug" when the jack cable is unplugged, so we can just create a new script listen-jack.sh with the following:

acpi_listen | \
( while true
    do read X
    if echo $X | grep "jack/headphone HEADPHONE unplug" &> /dev/null; then
        echo "pause"
    elif echo $X | grep "jack/headphone HEADPHONE plug" &> /dev/null; then
        echo "play"
    fi
done )

Then plug it in our original script:

#!/bin/bash

players=("spotify" "chrome" "mpv")
last_playing=()

(./listen-lock.sh & ./listen-jack.sh) | \
( while true
    do read X
    if echo $X | grep "pause" &> /dev/null; then
        last_playing=()
        for player in "${players[@]}"; do
            status=$((
                dbus-send \
                --print-reply \
                --dest="org.mpris.MediaPlayer2.$player" \
                /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus' \
                2>/dev/null | tail -1 | cut -d\" -f2
            ) || echo "")
            if [ "$status" = "Playing" ]; then
                last_playing+=($player)
                dbus-send --print-reply --dest="org.mpris.MediaPlayer2.$player" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause
            fi
        done
        if [ ${#last_playing[@]} -ne 0 ]; then
            notify-send -i dialog-info -u low -t 1000 "Paused" "${last_playing[@]}"
        fi
    elif echo $X | grep "play" &> /dev/null; then
        for player in "${last_playing[@]}"; do
            dbus-send --print-reply --dest="org.mpris.MediaPlayer2.$player" /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Play
        done
        if [ ${#last_playing[@]} -ne 0 ]; then
            notify-send -i dialog-info -u low -t 1000 "Play" "${last_playing[@]}"
        fi
    fi
done )

As a bonus I also added notifications!

Conclusion

I hope this article helped you solve a need that I think a lot of people don’t know they have until they’re using a similar solution. Coming back to the same music you were listening to before going AFK is one of the good feeling in life. Cheers!