diff options
Diffstat (limited to 'src/mpris.rs')
| -rw-r--r-- | src/mpris.rs | 71 |
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; | |||
| 22 | use std::sync::atomic::AtomicBool; | 22 | use std::sync::atomic::AtomicBool; |
| 23 | use std::sync::{Arc, Mutex, RwLock}; | 23 | use std::sync::{Arc, Mutex, RwLock}; |
| 24 | use std::thread; | 24 | use std::thread; |
| 25 | use std::time::Duration; | 25 | use std::time::{Duration, Instant}; |
| 26 | use zbus::blocking::Connection; | 26 | use zbus::blocking::Connection; |
| 27 | use zbus::interface; | 27 | use zbus::interface; |
| 28 | use zbus::zvariant::{ObjectPath, Value}; | 28 | use 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`. | ||
| 106 | const 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. |
| 106 | fn build_metadata(track: &Track) -> HashMap<String, Value<'static>> { | 110 | fn 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; |
