Skip to content

Conversation

paulfariello
Copy link

@paulfariello paulfariello commented Sep 23, 2025

Taking back on @wisp3rwind work for adding MPRIS support #1341.

  • Rebased on recent dev branch
  • ensure cargo fmt and cargo clippy passes on each commit
  • fix a few todos
  • send signal when position changed
  • return error when MPRIS command are done in wrong context or internal fails
  • handle position
  • handle identity
  • choose biggest art url
  • ensure metadata are always up to date or at least not invalid
  • notify on various property change

Not sure about:

  • Sending current state of player for all new listener (59767ce)

@paulfariello paulfariello force-pushed the mpris branch 4 times, most recently from 434aff3 to cae96c8 Compare September 23, 2025 10:34
paulfariello and others added 10 commits September 23, 2025 12:35
- which is just a tokio::sync::mpsc sender, so this should be safe
- prep for MPRIS support, which will use this to control playback
- preparation for MPRIS support
- now that the data is there, also yield from player_event_handler
- preparation for MPRIS support
- following the spec at https://specifications.freedesktop.org/mpris-spec/latest/

- some properties/commands are not fully supported, yet
@paulfariello paulfariello changed the title Draft: Add MPRIS support Add MPRIS support Sep 30, 2025
@paulfariello
Copy link
Author

@roderickvd this PR can probably be considered ready. The only missing feature is to handle the track list. It can probably be done in another PR.

Right now, CI is failing but doesn't seems to be related to this specific PR.

Do you have an opinion concerning 59767ce which is a broader modification than just MPRIS support?

Copy link
Contributor

@wisp3rwind wisp3rwind left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for moving this forward!

I've looked through the PR superficially; here are a few comments. I didn't really check the actual implementation of MPRIS commands.

let _ = sender.send(PlayerEvent::Loading {
play_request_id,
track_id: track_id.clone(),
position_ms: 0, // TODO
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a quick look at the code, it might be viable to store position_ms in PlayerState::Loading at the end of handle_command_load to have access here?

OTOH, it's probably fine to not do that work here: This event should be followed by PlayerEvent::Playing or PlayerEvent::Paused quickly, and arguably, the position is not that important while loading.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in f5cf187

track_id: track_id.clone(),
});
}
_ => (),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about PlayerState::Stopped? It seems somewhat important, since it's the initial state of the Player; unfortunately, the data to fill in the missing fields of PlayerEvent::Stopped is not available in that case. Could it be filled with some dummy/clearly invalid values?1 Or maybe it's not so important after all, since the initial state of the MPRIS task will also correspond to a stopped state?

Footnotes

  1. In that case, maybe also map PlayerState::Invalid to PlayerEvent::Stopped?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 4f55158


PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender),
PlayerCommand::AddEventSender(sender) => {
// Send current player state to new event listener
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good idea to me 👍

src/main.rs Outdated
.optopt(
POSITION_UPDATE_SHORT,
POSITION_UPDATE,
"Update position interval in ms",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explain that this is about player events and thereby also MPRIS? I feel like without that context, it be quite unclear what the purpose of this option is?

Personally, I'd prefer the option to be --position-update-interval to make it really explicit, but it does become very long that way...

Copy link
Author

@paulfariello paulfariello Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 40ec0a3


let position_update_interval = opt_str(POSITION_UPDATE).as_deref().map(|position_update| {
match position_update.parse::<u64>() {
Ok(value) => Duration::from_millis(value),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add a lower bound here? Either by returning an error if lower, or maybe better by simply taking the value.min(MIN_INTERVAL)? (Not sure what value should be, a few ms at least?) Or at least require it to be nonzero?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea, what sensible value should we use? 100ms seems quite sensible lower bound.

Comment on lines 37 to 194
edition = "2024"

[features]
default = ["native-tls", "rodio-backend", "with-libmdns"]
default = ["native-tls", "rodio-backend", "with-libmdns", "with-mpris"]

# TLS backends (mutually exclusive - compile-time checks in oauth/src/lib.rs)
# Note: Feature validation is in oauth crate since it's compiled first in the dependency tree.
# See COMPILING.md for more details on TLS backend selection.

# native-tls: Uses the system's native TLS stack (OpenSSL on Linux, Secure Transport on macOS,
# SChannel on Windows). This is the default as it's well-tested, widely compatible, and integrates
# with system certificate stores. Choose this for maximum compatibility and when you want to use
# system-managed certificates.
native-tls = ["librespot-core/native-tls", "librespot-oauth/native-tls"]

# rustls-tls: Uses the Rust-based rustls TLS implementation with certificate authority (CA)
# verification. This provides a Rust TLS stack (with assembly optimizations). Choose this for
# avoiding external OpenSSL dependencies, reproducible builds, or when targeting platforms where
# native TLS dependencies are unavailable or problematic (musl, embedded, static linking).
#
# Two certificate store options are available:
#
# - rustls-tls-native-roots: Uses rustls with native system certificate stores (ca-certificates on
# Linux, Security.framework on macOS, Windows certificate store on Windows). Best for most users as
# it integrates with system-managed certificates and gets security updates through the OS.
rustls-tls-native-roots = [
"librespot-core/rustls-tls-native-roots",
"librespot-oauth/rustls-tls-native-roots",
]
# rustls-tls-webpki-roots: Uses rustls with Mozilla's compiled-in certificate store (webpki-roots).
# Best for reproducible builds, containerized environments, or when you want certificate handling
# to be independent of the host system.
rustls-tls-webpki-roots = [
"librespot-core/rustls-tls-webpki-roots",
"librespot-oauth/rustls-tls-webpki-roots",
]

# Audio backends - see README.md for audio backend selection guide
# Cross-platform backends:

# rodio-backend: Cross-platform audio backend using Rodio (default). Provides good cross-platform
# compatibility with automatic backend selection. Uses ALSA on Linux, WASAPI on Windows, CoreAudio
# on macOS.
rodio-backend = ["librespot-playback/rodio-backend"]

# rodiojack-backend: Rodio backend with JACK support for professional audio setups.
rodiojack-backend = ["librespot-playback/rodiojack-backend"]

# gstreamer-backend: Uses GStreamer multimedia framework for audio output.
# Provides extensive audio processing capabilities.
gstreamer-backend = ["librespot-playback/gstreamer-backend"]

# portaudio-backend: Cross-platform audio I/O library backend.
portaudio-backend = ["librespot-playback/portaudio-backend"]

# sdl-backend: Simple DirectMedia Layer audio backend.
sdl-backend = ["librespot-playback/sdl-backend"]

# Platform-specific backends:

# alsa-backend: Advanced Linux Sound Architecture backend (Linux only).
# Provides low-latency audio output on Linux systems.
alsa-backend = ["librespot-playback/alsa-backend"]

# pulseaudio-backend: PulseAudio backend (Linux only).
# Integrates with the PulseAudio sound server for advanced audio routing.
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]

# jackaudio-backend: JACK Audio Connection Kit backend.
# Professional audio backend for low-latency, high-quality audio routing.
jackaudio-backend = ["librespot-playback/jackaudio-backend"]

# Network discovery backends - choose one for Spotify Connect device discovery
# See COMPILING.md for dependencies and platform support.

# with-libmdns: Pure-Rust mDNS implementation (default).
# No external dependencies, works on all platforms. Choose this for simple deployments or when
# avoiding system dependencies.
with-libmdns = ["librespot-discovery/with-libmdns"]

# with-avahi: Uses Avahi daemon for mDNS (Linux only).
# Integrates with system's Avahi service for network discovery. Choose this when you want to
# integrate with existing Avahi infrastructure or need advanced mDNS features. Requires
# libavahi-client-dev.
with-avahi = ["librespot-discovery/with-avahi"]

# with-dns-sd: Uses DNS Service Discovery (cross-platform).
# On macOS uses Bonjour, on Linux uses Avahi compatibility layer. Choose this for tight system
# integration on macOS or when using Avahi's dns-sd compatibility mode on Linux.
with-dns-sd = ["librespot-discovery/with-dns-sd"]

# Audio processing features:

# passthrough-decoder: Enables direct passthrough of Ogg Vorbis streams without decoding.
# Useful for custom audio processing pipelines or when you want to handle audio decoding
# externally. When enabled, audio is not decoded by librespot but passed through as raw Ogg Vorbis
# data.
passthrough-decoder = ["librespot-playback/passthrough-decoder"]

# MPRIS: Allow external tool to have access to playback
# status, metadata and to control the player.
with-mpris = ["dep:zbus", "dep:zvariant"]

[lib]
name = "librespot"
path = "src/lib.rs"

[[bin]]
name = "librespot"
path = "src/main.rs"
doc = false

[workspace.dependencies]
librespot-audio = { version = "0.7.1", path = "audio", default-features = false }
librespot-connect = { version = "0.7.1", path = "connect", default-features = false }
librespot-core = { version = "0.7.1", path = "core", default-features = false }
librespot-discovery = { version = "0.7.1", path = "discovery", default-features = false }
librespot-metadata = { version = "0.7.1", path = "metadata", default-features = false }
librespot-oauth = { version = "0.7.1", path = "oauth", default-features = false }
librespot-playback = { version = "0.7.1", path = "playback", default-features = false }
librespot-protocol = { version = "0.7.1", path = "protocol", default-features = false }

[dependencies]
librespot-audio.workspace = true
librespot-connect.workspace = true
librespot-core.workspace = true
librespot-discovery.workspace = true
librespot-metadata.workspace = true
librespot-oauth.workspace = true
librespot-playback.workspace = true
librespot-protocol.workspace = true

data-encoding = "2.5"
env_logger = { version = "0.11.2", default-features = false, features = [
"color",
"humantime",
"auto-color",
] }
futures-util = { version = "0.3", default-features = false }
getopts = "0.2"
log = "0.4"
sha1 = "0.10"
sysinfo = { version = "0.36", default-features = false, features = ["system"] }
thiserror = "2"
tokio = { version = "1", features = [
"rt",
"macros",
"signal",
"sync",
"process",
] }
time = { version = "0.3", features = ["formatting"] }
url = "2.2"
zbus = { version = "4", default-features = false, features = ["tokio"], optional = true }
zvariant = { version = "4", default-features = false, optional = true }

[package.metadata.deb]
maintainer = "Librespot Organization <[email protected]>"
Copy link
Contributor

@wisp3rwind wisp3rwind Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should upgrade these to "5.x.y": That's also what discovery/Cargo.toml specifies for the with-avahi feature.

Maybe, that should be added to the global Cargo.toml as a workspace dependency which we reference here to be sure to unify versions? I'm don't really know how workspace deps work in the case of optional dependencies, so I'm not completely sure that this works out.

EDIT: Not sure what Github is doing here; this comment refers to zbus and zvariant.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgraded to 5 in 280cf1b

.pause()
.map_err(|err| zbus::fdo::Error::Failed(format!("{err}"))),
(Some(_), None) => {
zbus::fdo::Result::Err(zbus::fdo::Error::Failed(String::from("No track")))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe import these globally as

use zbus::fdo::{Result as FdoResult, Error as FdoError};

to make this code a little more compact and readable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a7a65b0, aliasing to FdoResult seems to break zbus::interface macro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants