i3 Desktop Part 4: Media Controls and Radio

Media controls are one of those desktop features I only notice when they are wrong.

If play/pause works in the browser but not the music player, it is annoying. If the status bar shows stale track data, I stop trusting it. If internet radio needs a separate terminal command every time, I stop using it.

The setup now has two connected pieces: a general media controller and a dedicated radio menu.

The general controller is .local/bin/i3-media-controller. It uses playerctl for MPRIS-compatible apps such as browsers, Spotify, VLC, and anything else exposing a media session.

The radio controller is .local/bin/i3-radio-menu. It uses mpv for playback, with yt-dlp support for YouTube playlist URLs.

The useful part is that the two cooperate.

Normal media first

The media controller has the usual actions:

  • play/pause
  • play
  • pause
  • next
  • previous
  • stop
  • status
  • select player
  • clear selected player

By default it uses the active player. If more than one player exists, I can choose a target player through the menu. That choice is stored under the runtime directory, so the controller can keep using it until I clear it or the player disappears.

This solves a real annoyance. Sometimes a browser tab, video player, and music app all expose MPRIS at the same time. Auto-detection is usually fine, but when it is not, selecting the target player is better than fighting with focus.

The status bar media block uses the same idea. It finds the selected player if there is one, otherwise finds the playing player, and shows the artist and title while something is actually playing.

Radio as a first-class feature

Internet radio is separate because it is not naturally an MPRIS player in this setup.

The station list lives in ~/.config/i3/radio-stations.tsv. It is local-only on purpose. The dotfiles repo keeps an example template, but the real station list is not committed. Each row is:

Name<TAB>Stream or playlist URL

The radio script can create the file from the template if it is missing. It can play a station, stop, pause, resume, go next, go previous, show status, and open the station file in a terminal editor.

Playback is handled by mpv with no video, best available audio, no forced window, and an IPC socket. Runtime state is stored under XDG_RUNTIME_DIR/i3-radio: PID, station name, URL, socket, paused marker, and log file.

That state is what lets the bar and media keys understand radio.

One set of media keys

The nice detail is in the media controller.

Before sending a command to playerctl, it checks whether radio should handle playback. Radio gets control when a radio process is running, no specific MPRIS player has been selected, and no normal MPRIS player is currently playing.

That means the same keybindings work:

  • play/pause toggles radio when radio is the active context
  • next and previous step through the current radio playlist when possible
  • stop stops radio

If a browser or music app is playing, media keys control that instead. If I explicitly select a player, that player wins.

This keeps the mental model small. I do not need one set of keys for “media” and another for “radio” most of the time. The desktop chooses the sensible target.

The bar stays quiet

The media block appears only while a normal player is playing.

The radio block appears only while radio is running. It shows the station name, changes when paused, and opens the radio menu on click. Middle click stops radio.

Both blocks are refreshed with real-time i3blocks signals when playback state changes. Starting radio sends the radio refresh signal. Media actions refresh the media block. The i3-stop-playback helper refreshes both.

That makes the bar feel immediate without polling every second.

Stopping playback before lock and power actions

There is also a practical safety detail.

Before locking, suspending, logging out, rebooting, or shutting down, i3-stop-playback stops radio and pauses normal media players. This avoids the classic problem of audio carrying on because the screen locked but the session did not really stop.

The script is cautious. It checks whether the radio PID still belongs to mpv, cleans up stale state, then stops it. For normal media players, it pauses anything currently playing through playerctl.

Why this works

This is a good example of the whole i3 desktop approach.

The implementation uses standard tools: playerctl, mpv, yt-dlp, socat, shell scripts, rofi, and dunst. The custom part is the glue: choosing the active context, keeping runtime state, updating the bar, and giving everything a menu.

That is enough to make media feel integrated without building a media app.