A cross-platform library for handling OS media controls and metadata.
  • Rust 98.9%
  • Shell 1.1%
Find a file
2026-03-18 16:11:33 +08:00
.github/workflows Add CI workflow for auto-releases 2025-06-24 14:26:05 +08:00
docs Add diagram file 2026-03-18 16:11:33 +08:00
examples Rename example 2023-12-21 17:09:43 -03:00
src run cargo fmt 2025-06-23 16:10:30 +08:00
tests Add playerctl test script 2024-01-01 22:23:32 -03:00
.gitattributes Add gitattributes for line endings 2025-06-23 16:03:37 +08:00
.gitignore Added Cargo.lock to gitignore 2021-06-28 21:54:01 -04:00
build.rs Add gitattributes for line endings 2025-06-23 16:03:37 +08:00
Cargo.toml Release 0.8.3 2025-06-24 13:16:37 +08:00
CHANGELOG.md Release 0.8.3 2025-06-24 13:16:37 +08:00
LICENSE Added license 2021-01-27 23:32:04 -03:00
README.md Add diagram in README 2026-03-18 16:10:58 +08:00
rust-toolchain.toml Bump MSRV to 1.67 2024-01-28 14:49:46 -03:00
rustfmt.toml Add rustfmt file to standardize formats 2025-06-23 15:05:23 +08:00

Souvlaki Crates.io Docs CI

A cross-platform library for handling OS media controls and metadata. One abstraction for Linux, MacOS/iOS, Windows.

Architecture diagram for souvlaki.

Supported platforms

  • Linux/BSD (via MPRIS)
  • MacOS/iOS
  • Windows

Windows

  • Update metadata:
    image
  • Play and pause polling.
    play_pause

Linux

  • GNOME:
    GNOME

  • playerctl:

# In one shell
$ cd souvlaki 
$ cargo run --example window

# In another shell
$ playerctl metadata
my_player xesam:artist              Slowdive
my_player xesam:album               Souvlaki
my_player mpris:artUrl              https://c.pxhere.com/photos/34/c1/souvlaki_authentic_greek_greek_food_mezes-497780.jpg!d
my_player mpris:trackid             '/'
my_player mpris:length              290000000
my_player xesam:title               When The Sun Hits

MacOS

  • Control Center:
    Control Center
  • Now Playing:
    Now Playing

Requirements

Minimum supported Rust version is 1.67.

Usage

The main struct is MediaControls. In order to create this struct you need a PlatformConfig. This struct contains all of the platform-specific requirements for spawning media controls. Here are the differences between the platforms:

  • MacOS: No config needed, but requires an AppDelegate/winit event loop (an open window is not required.) (#23)
  • iOS: No config needed.
  • Linux:
    • dbus_name: The way your player will appear on D-Bus. It should follow the D-Bus specification.
    • display_name: This could be however you want. It's the name that will be shown to the users.
  • Windows:
    • hwnd: In this platform, a window needs to be opened to create media controls. The argument required is an HWND, a value of type *mut c_void. This value can be extracted when you open a window in your program, for example using the raw_window_handle in winit.

Linux backends: D-Bus and zbus

When using the library on Linux, the default backend is dbus-crossroads. This backend has some issues with consistency in general, but is more stable and uses the native D-Bus library behind the scenes. The zbus backend however, is more modern and is written in pure Rust. It spawns another thread and stars an async pollster runtime, handling the incoming MPRIS messages.

To enable the zbus backend, in your Cargo.toml, set default-features to false and enable the use_zbus feature:

souvlaki = { version = "<version>", default-features = false, features = ["use_zbus"] }

Note: If you think there's a better way of using the zbus library regarding the async runtime in another thread, feel free to leave a PR or issue.

Example

use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, PlatformConfig};

fn main() {
    #[cfg(not(target_os = "windows"))]
    let hwnd = None;

    #[cfg(target_os = "windows")]
    let hwnd = {
        use raw_window_handle::windows::WindowsHandle;

        let handle: WindowsHandle = unimplemented!();
        Some(handle.hwnd)
    };

    let config = PlatformConfig {
        dbus_name: "my_player",
        display_name: "My Player",
        hwnd,
    };

    let mut controls = MediaControls::new(config).unwrap();

    // The closure must be Send and have a static lifetime.
    controls
        .attach(|event: MediaControlEvent| println!("Event received: {:?}", event))
        .unwrap();

    // Update the media metadata.
    controls
        .set_metadata(MediaMetadata {
            title: Some("Souvlaki Space Station"),
            artist: Some("Slowdive"),
            album: Some("Souvlaki"),
            ..Default::default()
        })
        .unwrap();

    // Your actual logic goes here.
    loop {
        std::thread::sleep(std::time::Duration::from_secs(1));
    }

    // The controls automatically detach on drop.
}

Check out this example here.

Thanks 💗

  • To jpochyla for being a contributor to library architecture and the sole developer of MacOS support.