aboutsummaryrefslogtreecommitdiffstats
path: root/src/mpris.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/mpris.rs71
1 files changed, 50 insertions, 21 deletions
diff --git a/src/mpris.rs b/src/mpris.rs
index 3fc706d..de19113 100644
--- a/src/mpris.rs
+++ b/src/mpris.rs
@@ -22,7 +22,7 @@ use std::path::PathBuf;
22use std::sync::atomic::AtomicBool; 22use std::sync::atomic::AtomicBool;
23use std::sync::{Arc, Mutex, RwLock}; 23use std::sync::{Arc, Mutex, RwLock};
24use std::thread; 24use std::thread;
25use std::time::Duration; 25use std::time::{Duration, Instant};
26use zbus::blocking::Connection; 26use zbus::blocking::Connection;
27use zbus::interface; 27use zbus::interface;
28use zbus::zvariant::{ObjectPath, Value}; 28use zbus::zvariant::{ObjectPath, Value};
@@ -101,14 +101,17 @@ pub struct MprisPlayer {
101 loop_status: Arc<Mutex<String>>, 101 loop_status: Arc<Mutex<String>>,
102} 102}
103 103
104/// `mpris:trackid` advertised for whatever is playing. The spec only requires it to be a stable
105/// object path that changes per track; clients echo it back in `SetPosition`.
106const CURRENT_TRACK_ID: &str = "/org/mpris/MediaPlayer2/TrackList/CurrentTrack";
107
104/// Build the `Metadata` map (a{sv}) required by MPRIS, using only owned values 108/// Build the `Metadata` map (a{sv}) required by MPRIS, using only owned values
105/// so no borrowing from locals. 109/// so no borrowing from locals.
106fn build_metadata(track: &Track) -> HashMap<String, Value<'static>> { 110fn build_metadata(track: &Track) -> HashMap<String, Value<'static>> {
107 let mut map: HashMap<String, Value<'static>> = HashMap::new(); 111 let mut map: HashMap<String, Value<'static>> = HashMap::new();
108 112
109 // `mpris:trackid` is typed as an object path (`o`) in the spec, not as a string. 113 // `mpris:trackid` is typed as an object path (`o`) in the spec, not as a string.
110 let trackid = ObjectPath::try_from("/org/mpris/MediaPlayer2/TrackList/CurrentTrack") 114 let trackid = ObjectPath::try_from(CURRENT_TRACK_ID).expect("static object path");
111 .expect("static object path");
112 map.insert("mpris:trackid".into(), Value::new(trackid)); 115 map.insert("mpris:trackid".into(), Value::new(trackid));
113 116
114 let length_us = track 117 let length_us = track
@@ -273,12 +276,23 @@ impl MprisPlayer {
273 } 276 }
274 } 277 }
275 278
276 fn seek(&self, _offset: i64) { 279 fn seek(&self, offset: i64) {
277 // Seeking not implemented yet. 280 let target = (self.audio.position() + offset as f64 / 1_000_000.0).max(0.0);
281 crate::debug_eprintln(format_args!("mpris: seek by {}us", offset));
282 self.audio.seek(target);
278 } 283 }
279 284
280 fn set_position(&self, _track_id: zbus::zvariant::ObjectPath<'_>, _position: i64) { 285 fn set_position(&self, track_id: zbus::zvariant::ObjectPath<'_>, position: i64) {
281 // Seeking not implemented yet. 286 // The spec says to ignore the request when the id is not the track we are playing.
287 if track_id.as_str() != CURRENT_TRACK_ID {
288 crate::debug_eprintln(format_args!(
289 "mpris: ignoring SetPosition for foreign track {}",
290 track_id.as_str()
291 ));
292 return;
293 }
294 crate::debug_eprintln(format_args!("mpris: set position to {}us", position));
295 self.audio.seek((position as f64 / 1_000_000.0).max(0.0));
282 } 296 }
283 297
284 fn open_uri(&self, uri: String) { 298 fn open_uri(&self, uri: String) {
@@ -446,7 +460,7 @@ impl MprisPlayer {
446 460
447 #[zbus(property)] 461 #[zbus(property)]
448 fn can_seek(&self) -> bool { 462 fn can_seek(&self) -> bool {
449 false 463 true
450 } 464 }
451 465
452 #[zbus(property)] 466 #[zbus(property)]
@@ -543,6 +557,7 @@ pub fn start_mpris_server(
543 let mut last_volume: Option<f64> = None; 557 let mut last_volume: Option<f64> = None;
544 let mut last_shuffle: Option<ShuffleMode> = None; 558 let mut last_shuffle: Option<ShuffleMode> = None;
545 let mut last_loop: Option<String> = None; 559 let mut last_loop: Option<String> = None;
560 let mut last_position: Option<(Instant, i64)> = None;
546 561
547 while !shutdown_flag.load(std::sync::atomic::Ordering::SeqCst) { 562 while !shutdown_flag.load(std::sync::atomic::Ordering::SeqCst) {
548 let status = playback_status(&poll_audio); 563 let status = playback_status(&poll_audio);
@@ -610,25 +625,39 @@ pub fn start_mpris_server(
610 } 625 }
611 } 626 }
612 627
613 // A different track (or none at all) means the playhead jumped, which is what 628 // A different track, or a position that is nowhere near where the playhead
614 // `Seeked` exists for. Clients use it to reset their progress bar without 629 // should be by now, means it jumped — which is what `Seeked` exists for. Clients
615 // having to notice that two consecutive tracks happen to share a length. 630 // use it to reset their progress bar without having to notice that two
616 if track_changed { 631 // consecutive tracks happen to share a length. The steady tick is intentionally
617 let position_us = if track.is_some() { 632 // not signalled: the spec tells clients to interpolate while we report `Playing`.
618 (poll_audio.position() * 1_000_000.0) as i64 633 let position_us = if track.is_some() {
619 } else { 634 (poll_audio.position() * 1_000_000.0) as i64
620 0 635 } else {
621 }; 636 0
622 if let Err(e) = conn.emit_signal( 637 };
638 let jumped = match last_position {
639 Some((at, seen)) if !track_changed => {
640 let expected = if status == "Playing" {
641 seen + at.elapsed().as_micros() as i64
642 } else {
643 seen
644 };
645 (position_us - expected).abs() > 1_500_000
646 }
647 _ => false,
648 };
649 if (track_changed || jumped)
650 && let Err(e) = conn.emit_signal(
623 None::<&str>, 651 None::<&str>,
624 "/org/mpris/MediaPlayer2", 652 "/org/mpris/MediaPlayer2",
625 "org.mpris.MediaPlayer2.Player", 653 "org.mpris.MediaPlayer2.Player",
626 "Seeked", 654 "Seeked",
627 &(position_us,), 655 &(position_us,),
628 ) { 656 )
629 crate::debug_eprintln(format_args!("mpris: failed to emit Seeked: {}", e)); 657 {
630 } 658 crate::debug_eprintln(format_args!("mpris: failed to emit Seeked: {}", e));
631 } 659 }
660 last_position = Some((Instant::now(), position_us));
632 661
633 last_status = Some(status); 662 last_status = Some(status);
634 last_track = track; 663 last_track = track;