-
Notifications
You must be signed in to change notification settings - Fork 747
Add MPRIS support #1596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Add MPRIS support #1596
Conversation
434aff3
to
cae96c8
Compare
- 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
@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? |
There was a problem hiding this 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.
playback/src/player.rs
Outdated
let _ = sender.send(PlayerEvent::Loading { | ||
play_request_id, | ||
track_id: track_id.clone(), | ||
position_ms: 0, // TODO |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in f5cf187
playback/src/player.rs
Outdated
track_id: track_id.clone(), | ||
}); | ||
} | ||
_ => (), |
There was a problem hiding this comment.
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
-
In that case, maybe also map
PlayerState::Invalid
toPlayerEvent::Stopped
? ↩
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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]>" |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
src/mpris_event_handler.rs
Outdated
.pause() | ||
.map_err(|err| zbus::fdo::Error::Failed(format!("{err}"))), | ||
(Some(_), None) => { | ||
zbus::fdo::Result::Err(zbus::fdo::Error::Failed(String::from("No track"))) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Taking back on @wisp3rwind work for adding MPRIS support #1341.
Not sure about: