Skip to main content

crabshell_services/
mpris.rs

1//! Media players over MPRIS: every program on the session bus that owns
2//! an `org.mpris.MediaPlayer2.*` name, such as a music player or a
3//! browser playing a video.
4//!
5//! The players are listed with `ListNames` and followed through the
6//! bus's `NameOwnerChanged`. Each is read whole when it appears, then
7//! kept up to date from its `PropertiesChanged` and `Seeked`. MPRIS never
8//! says when the position moves, so it is read again when a player
9//! starts, stops, changes speed or changes track, and
10//! [`Player::position_at`] works out the rest.
11//!
12//! Everything runs on the services' executor, on the main thread. One
13//! stream reads every message the bus sends this connection, so a quiet
14//! bus leaves the shell asleep.
15
16#[cfg(test)]
17mod tests;
18
19use std::cell::RefCell;
20use std::future::poll_fn;
21use std::pin::Pin;
22use std::rc::Rc;
23use std::sync::Once;
24use std::time::{Duration, Instant};
25
26use crabshell_reactive::{ReadSignal, Signal};
27use crabshell_ui::Cx;
28use zbus::export::futures_core::Stream;
29use zbus::message::Type as Kind;
30use zbus::{
31  Array, Connection, DynamicType, Message, MessageStream, ObjectPath, OwnedObjectPath,
32  OwnedVariant, Variant,
33};
34
35use crate::Shared;
36use crate::dbus::{self, BUS, BUS_PATH, CALL_TIMEOUT, PROPERTIES, Props, set_changed};
37
38/// What every player's bus name starts with.
39const PREFIX: &str = "org.mpris.MediaPlayer2.";
40/// playerctld mirrors whichever player was active last, so showing it
41/// would show that player twice.
42const PLAYERCTLD: &str = "org.mpris.MediaPlayer2.playerctld";
43const PATH: &str = "/org/mpris/MediaPlayer2";
44/// The interface with the player's name and `Raise`.
45const ROOT: &str = "org.mpris.MediaPlayer2";
46const PLAYER: &str = "org.mpris.MediaPlayer2.Player";
47
48const MAX_PLAYERS: usize = 32;
49const MAX_ARTISTS: usize = 32;
50/// The longest text kept, in bytes.
51const MAX_TEXT: usize = 4096;
52
53/// Whether a player plays, from MPRIS's `PlaybackStatus`.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum PlaybackStatus {
56  /// Playing.
57  Playing,
58  /// Paused, to go on from where it is.
59  Paused,
60  /// Stopped, or a status this list doesn't know.
61  Stopped,
62}
63
64/// What a player does at the end, from MPRIS's `LoopStatus`.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum LoopStatus {
67  /// Stops at the end of the playlist.
68  None,
69  /// Plays the track again.
70  Track,
71  /// Plays the playlist again.
72  Playlist,
73}
74
75impl LoopStatus {
76  fn parse(name: &str) -> Option<LoopStatus> {
77    match name {
78      "None" => Some(LoopStatus::None),
79      "Track" => Some(LoopStatus::Track),
80      "Playlist" => Some(LoopStatus::Playlist),
81      _ => None,
82    }
83  }
84
85  fn name(self) -> &'static str {
86    match self {
87      LoopStatus::None => "None",
88      LoopStatus::Track => "Track",
89      LoopStatus::Playlist => "Playlist",
90    }
91  }
92}
93
94/// A media player and its track, as MPRIS describes them. Text is cut to
95/// 4 KiB.
96#[derive(Clone, Debug, PartialEq)]
97pub struct Player {
98  /// Its bus name, such as `org.mpris.MediaPlayer2.vlc`, unique while it
99  /// runs. The key to give `children_keyed`, and what
100  /// [`Mpris::play_pause`] and the rest take.
101  pub id: String,
102  /// Its name for people, such as `VLC media player`.
103  pub identity: String,
104  /// Its desktop file's name without `.desktop`, such as `vlc`, or `""`.
105  pub desktop_entry: String,
106  /// Whether it plays.
107  pub status: PlaybackStatus,
108  /// The track's title, or `""`.
109  pub title: String,
110  /// The track's artists, at most 32.
111  pub artists: Vec<String>,
112  /// The track's album, or `""`.
113  pub album: String,
114  /// Where the track's cover art is, such as a `file://` URL, or `""`.
115  pub art_url: String,
116  /// How long the track is, if the player says.
117  pub length: Option<Duration>,
118  /// Whether [`Mpris::play`] would do anything.
119  pub can_play: bool,
120  /// Whether [`Mpris::pause`] would do anything.
121  pub can_pause: bool,
122  /// Whether [`Mpris::next`] would do anything.
123  pub can_go_next: bool,
124  /// Whether [`Mpris::previous`] would do anything.
125  pub can_go_previous: bool,
126  /// Whether [`Mpris::seek`] and [`Mpris::set_position`] would do
127  /// anything.
128  pub can_seek: bool,
129  /// Whether it can be controlled at all.
130  pub can_control: bool,
131  /// Its volume, 1.0 for full, if it has one.
132  pub volume: Option<f64>,
133  /// Whether it shuffles, if it can.
134  pub shuffle: Option<bool>,
135  /// What it does at the end, if it can loop.
136  pub loop_status: Option<LoopStatus>,
137  /// How fast it plays, 1.0 for normal speed.
138  pub rate: f64,
139  /// Where playback was at [`position_known_at`](Self::position_known_at).
140  pub position: Duration,
141  /// When [`position`](Self::position) was read.
142  pub position_known_at: Instant,
143}
144
145impl Player {
146  /// Where playback is at `now`: [`position`](Self::position), moved on
147  /// at [`rate`](Self::rate) since it was read while the player plays,
148  /// and never past the track's length. A bar that shows it ticking
149  /// calls this from a clock of its own.
150  pub fn position_at(&self, now: Instant) -> Duration {
151    let mut position = self.position;
152    if self.status == PlaybackStatus::Playing {
153      let elapsed = now.saturating_duration_since(self.position_known_at);
154      let moved = position.as_secs_f64() + elapsed.as_secs_f64() * self.rate;
155      position = Duration::try_from_secs_f64(moved.max(0.0)).unwrap_or(position);
156    }
157    self.length.map_or(position, |length| position.min(length))
158  }
159
160  fn from_props(
161    id: String,
162    (root, props): &(Props, Props),
163    position: Duration,
164    at: Instant,
165  ) -> Player {
166    let metadata = metadata(props);
167    let flag = |name| dbus::get(props, name).unwrap_or(false);
168    Player {
169      id,
170      identity: text(root, "Identity"),
171      desktop_entry: text(root, "DesktopEntry"),
172      status: match dbus::get::<&str>(props, "PlaybackStatus") {
173        Some("Playing") => PlaybackStatus::Playing,
174        Some("Paused") => PlaybackStatus::Paused,
175        _ => PlaybackStatus::Stopped,
176      },
177      title: text(&metadata, "xesam:title"),
178      artists: dbus::get::<&Array>(&metadata, "xesam:artist").map_or(Vec::new(), |artists| {
179        let names = artists
180          .inner()
181          .iter()
182          .filter_map(|name| <&str>::try_from(name).ok());
183        names.take(MAX_ARTISTS).map(cut).collect()
184      }),
185      album: text(&metadata, "xesam:album"),
186      // A cut URL would point somewhere else.
187      art_url: dbus::get::<&str>(&metadata, "mpris:artUrl")
188        .filter(|url| url.len() <= MAX_TEXT)
189        .unwrap_or_default()
190        .to_owned(),
191      // Some players say 0 for a stream.
192      length: metadata
193        .get("mpris:length")
194        .and_then(|length| micros(length))
195        .filter(|length| !length.is_zero()),
196      can_play: flag("CanPlay"),
197      can_pause: flag("CanPause"),
198      can_go_next: flag("CanGoNext"),
199      can_go_previous: flag("CanGoPrevious"),
200      can_seek: flag("CanSeek"),
201      can_control: flag("CanControl"),
202      volume: dbus::get::<f64>(props, "Volume").filter(|volume| volume.is_finite()),
203      shuffle: dbus::get(props, "Shuffle"),
204      loop_status: dbus::get::<&str>(props, "LoopStatus").and_then(LoopStatus::parse),
205      rate: dbus::get::<f64>(props, "Rate")
206        .filter(|rate| rate.is_finite())
207        .unwrap_or(1.0),
208      position,
209      position_known_at: at,
210    }
211  }
212}
213
214/// The track's `Metadata`, an `a{sv}`, or nothing when it is of another
215/// type.
216fn metadata(props: &Props) -> Props {
217  let metadata = props
218    .get("Metadata")
219    .and_then(|value| value.try_clone().ok());
220  metadata
221    .and_then(|value| Props::try_from(value).ok())
222    .unwrap_or_default()
223}
224
225/// The text `name`, cut to [`MAX_TEXT`], or `""`.
226fn text(props: &Props, name: &str) -> String {
227  cut(dbus::get::<&str>(props, name).unwrap_or_default())
228}
229
230/// `text` cut to [`MAX_TEXT`] bytes, at a char boundary.
231fn cut(text: &str) -> String {
232  text[..text.floor_char_boundary(MAX_TEXT)].to_owned()
233}
234
235/// A time in microseconds, as MPRIS gives them, of any integer type.
236/// `None` for a negative one or another type.
237fn micros(value: &Variant<'_>) -> Option<Duration> {
238  let micros = match *value {
239    Variant::I64(n) => u64::try_from(n).ok()?,
240    Variant::U64(n) => n,
241    Variant::I32(n) => u64::try_from(n).ok()?,
242    Variant::U32(n) => n.into(),
243    Variant::I16(n) => u64::try_from(n).ok()?,
244    Variant::U16(n) => n.into(),
245    Variant::U8(n) => n.into(),
246    _ => return None,
247  };
248  Some(Duration::from_micros(micros))
249}
250
251/// The media players, from `cx.mpris()`. A clone is cheap and shares the
252/// one connection.
253#[derive(Clone)]
254pub struct Mpris {
255  /// `None` when the shell provided no services.
256  service: Option<Rc<Service>>,
257  players: ReadSignal<Vec<Player>>,
258}
259
260impl Mpris {
261  /// Every player, in the order they appeared on the bus. Empty while
262  /// there is no session bus. A player shows once it is read.
263  pub fn players(&self) -> ReadSignal<Vec<Player>> {
264    self.players
265  }
266
267  /// Tells player `id` to play if it is paused, and to pause if it
268  /// plays. A refusal or an unknown `id` is logged, as for every method
269  /// here.
270  pub fn play_pause(&self, id: &str) {
271    self.call(id, PLAYER, "PlayPause", ());
272  }
273
274  /// Tells player `id` to play.
275  pub fn play(&self, id: &str) {
276    self.call(id, PLAYER, "Play", ());
277  }
278
279  /// Tells player `id` to pause.
280  pub fn pause(&self, id: &str) {
281    self.call(id, PLAYER, "Pause", ());
282  }
283
284  /// Tells player `id` to stop.
285  pub fn stop(&self, id: &str) {
286    self.call(id, PLAYER, "Stop", ());
287  }
288
289  /// Tells player `id` to go to the next track.
290  pub fn next(&self, id: &str) {
291    self.call(id, PLAYER, "Next", ());
292  }
293
294  /// Tells player `id` to go to the previous track.
295  pub fn previous(&self, id: &str) {
296    self.call(id, PLAYER, "Previous", ());
297  }
298
299  /// Moves player `id` `seconds` on from where it is, or back for a
300  /// negative number. One that isn't finite isn't sent.
301  pub fn seek(&self, id: &str, seconds: f64) {
302    if !seconds.is_finite() {
303      return tracing::warn!(id, seconds, "a seek by no finite time; not sent");
304    }
305    let offset = (seconds * 1e6).round() as i64;
306    self.call(id, PLAYER, "Seek", offset);
307  }
308
309  /// Moves player `id` to `position` in its track. A player that names
310  /// no track can't be moved this way, which is logged.
311  pub fn set_position(&self, id: &str, position: Duration) {
312    let track = self.service.as_ref().and_then(|service| service.track(id));
313    let Some(track) = track else {
314      return tracing::warn!(id, "the media player names no track; no position set");
315    };
316    let at = i64::try_from(position.as_micros()).unwrap_or(i64::MAX);
317    self.call(id, PLAYER, "SetPosition", (track, at));
318  }
319
320  /// Sets player `id`'s volume, 1.0 for full. One that isn't finite
321  /// isn't sent.
322  pub fn set_volume(&self, id: &str, volume: f64) {
323    if !volume.is_finite() {
324      return tracing::warn!(id, volume, "a volume that isn't finite; not sent");
325    }
326    self.set(id, "Volume", Variant::from(volume));
327  }
328
329  /// Turns player `id`'s shuffle on or off.
330  pub fn set_shuffle(&self, id: &str, shuffle: bool) {
331    self.set(id, "Shuffle", Variant::from(shuffle));
332  }
333
334  /// Sets what player `id` does at the end.
335  pub fn set_loop(&self, id: &str, status: LoopStatus) {
336    self.set(id, "LoopStatus", Variant::from(status.name()));
337  }
338
339  /// Asks player `id` to show its window.
340  pub fn raise(&self, id: &str) {
341    self.call(id, ROOT, "Raise", ());
342  }
343
344  /// The service and its connection, if player `id` is on the bus. An
345  /// unknown `id` is logged.
346  fn target(&self, id: &str, method: &str) -> Option<(Rc<Shared>, Connection)> {
347    let service = self.service.as_ref()?;
348    let state = service.state.borrow();
349    let target = state
350      .as_ref()
351      .filter(|state| state.entries.iter().any(|entry| entry.id == id))
352      .and_then(|state| state.connection.clone());
353    if target.is_none() {
354      tracing::warn!(id, method, "no such media player");
355    }
356    Some((Rc::clone(&service.shared), target?))
357  }
358
359  /// Calls `method` on player `id`, without waiting for the answer.
360  fn call<B>(&self, id: &str, interface: &'static str, method: &'static str, body: B)
361  where
362    B: serde::Serialize + DynamicType + 'static,
363  {
364    let Some((shared, connection)) = self.target(id, method) else {
365      return;
366    };
367    let id = id.to_owned();
368    let spawner = Rc::clone(&shared);
369    spawner.spawn(async move {
370      let call = connection.call_method(Some(id.as_str()), PATH, Some(interface), method, &body);
371      if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
372        tracing::info!(id, method, %error, "the media player refused");
373      }
374    });
375  }
376
377  /// Sets player `id`'s property `name`, without waiting for the answer.
378  fn set(&self, id: &str, name: &'static str, value: Variant<'static>) {
379    let Some((shared, connection)) = self.target(id, name) else {
380      return;
381    };
382    let id = id.to_owned();
383    let spawner = Rc::clone(&shared);
384    spawner.spawn(async move {
385      let set = dbus::set(&shared, &connection, &id, (PATH, PLAYER), name, value);
386      if let Err(error) = set.await {
387        tracing::info!(id, name, %error, "the media player refused");
388      }
389    });
390  }
391}
392
393/// `cx.mpris()`.
394pub(crate) fn mpris(cx: &Cx) -> Mpris {
395  let Some(service) = cx.service::<Service>() else {
396    static LOGGED: Once = Once::new();
397    LOGGED.call_once(|| {
398      tracing::warn!("cx.mpris() needs the services the shell provides; it stays empty");
399    });
400    return Mpris {
401      service: None,
402      players: cx.signal(Vec::new()).read_only(),
403    };
404  };
405  let players = service.start();
406  Mpris {
407    service: Some(service),
408    players,
409  }
410}
411
412/// The media players of one shell, followed from the first ask.
413pub(crate) struct Service {
414  shared: Rc<Shared>,
415  state: RefCell<Option<State>>,
416}
417
418struct State {
419  players: Signal<Vec<Player>>,
420  /// The session bus, once joined.
421  connection: Option<Connection>,
422  /// Every player on the bus, in the order they appeared, read or not.
423  entries: Vec<Entry>,
424}
425
426struct Entry {
427  /// Its bus name.
428  id: String,
429  /// The unique name that owns it, which its signals come from. `None`
430  /// until the bus or a read says.
431  owner: Option<String>,
432  /// Counts reads, so a slow reply never overwrites a newer one.
433  reads: u64,
434  /// Its `org.mpris.MediaPlayer2` and `Player` properties. `None` until
435  /// a read works.
436  props: Option<(Props, Props)>,
437  /// The position as last read, and when.
438  position: (Duration, Instant),
439}
440
441impl Service {
442  pub(crate) fn new(shared: Rc<Shared>) -> Service {
443    Service {
444      shared,
445      state: RefCell::new(None),
446    }
447  }
448
449  /// The list, joining the bus if this is the first ask.
450  fn start(self: &Rc<Self>) -> ReadSignal<Vec<Player>> {
451    if let Some(state) = &*self.state.borrow() {
452      return state.players.read_only();
453    }
454    let players = self.shared.root.signal(Vec::new());
455    *self.state.borrow_mut() = Some(State {
456      players,
457      connection: None,
458      entries: Vec::new(),
459    });
460    match self.shared.config.session_bus.clone() {
461      Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
462      None => tracing::info!("no session bus, so there are no media players"),
463    }
464    players.read_only()
465  }
466
467  /// Whether the list is still there: its owner, the root, isn't disposed.
468  fn alive(&self) -> bool {
469    let state = self.state.borrow();
470    let alive = state
471      .as_ref()
472      .and_then(|state| state.players.try_with(|_| ()));
473    alive.is_some()
474  }
475
476  fn with_entry<R>(&self, id: &str, f: impl FnOnce(&mut Entry) -> R) -> Option<R> {
477    let mut state = self.state.borrow_mut();
478    let entry = state
479      .as_mut()?
480      .entries
481      .iter_mut()
482      .find(|entry| entry.id == id)?;
483    Some(f(entry))
484  }
485
486  /// The track player `id` plays, for `SetPosition`.
487  fn track(&self, id: &str) -> Option<OwnedObjectPath> {
488    self
489      .with_entry(id, |entry| {
490        let (_, props) = entry.props.as_ref()?;
491        let track = dbus::get::<&ObjectPath>(&metadata(props), "mpris:trackid")?.to_owned();
492        Some(OwnedObjectPath::from(track))
493      })
494      .flatten()
495  }
496
497  /// Subscribes to the players' names and signals, then reads every
498  /// player already on the bus.
499  async fn join(self: &Rc<Self>, connection: &Connection) -> zbus::Result<()> {
500    let names = format!(
501      "type='signal',sender='{BUS}',interface='{BUS}',member='NameOwnerChanged',\
502       arg0namespace='{ROOT}'"
503    );
504    // From any sender: `on_signal` keeps the players' own.
505    let signals = format!("type='signal',path='{PATH}'");
506    for rule in [names, signals] {
507      dbus::bus_call(connection, "AddMatch", &rule).await?;
508    }
509    let call = connection.call_method(Some(BUS), BUS_PATH, Some(BUS), "ListNames", &());
510    let names: Vec<String> = call.await?.body().deserialize()?;
511    // Each read asks for its owner. Awaiting them here would leave the
512    // bus's messages unread, and zbus stops reading at 64.
513    for name in names.into_iter().filter(|name| is_player(name)) {
514      self.appeared(connection, name, None);
515    }
516    Ok(())
517  }
518
519  /// Player `id` is on the bus, newly or under a new `owner`.
520  fn appeared(self: &Rc<Self>, connection: &Connection, id: String, owner: Option<String>) {
521    {
522      let mut state = self.state.borrow_mut();
523      let Some(state) = state.as_mut() else {
524        return;
525      };
526      let full = state.entries.len() >= MAX_PLAYERS;
527      match state.entries.iter_mut().find(|entry| entry.id == id) {
528        Some(entry) if entry.owner == owner => return,
529        Some(entry) => entry.owner = owner,
530        None if full => {
531          return tracing::warn!(id, "too many media players; leaving one out");
532        }
533        None => state.entries.push(Entry {
534          id: id.clone(),
535          owner,
536          reads: 0,
537          props: None,
538          position: (Duration::ZERO, Instant::now()),
539        }),
540      }
541    }
542    self.read(connection, id);
543  }
544
545  /// Reads player `id` whole and shows it. A failed read is logged, and
546  /// the player keeps what it showed.
547  fn read(self: &Rc<Self>, connection: &Connection, id: String) {
548    let Some(read) = self.with_entry(&id, |entry| {
549      entry.reads += 1;
550      entry.reads
551    }) else {
552      return;
553    };
554    let (service, connection) = (Rc::clone(self), connection.clone());
555    self.shared.spawn(async move {
556      let shared = &service.shared;
557      let all = async {
558        // A new owner since then reads again, so this read is dropped.
559        let owner = dbus::owner(&connection, &id).await;
560        let root = dbus::get_all(shared, &connection, &id, PATH, ROOT).await?;
561        let props = dbus::get_all(shared, &connection, &id, PATH, PLAYER).await?;
562        zbus::Result::Ok((owner, (root, props)))
563      };
564      let (owner, props) = match all.await {
565        Ok(read) => read,
566        Err(error) => return tracing::warn!(id, %error, "can't read a media player"),
567      };
568      let position = props.1.get("Position").and_then(|at| micros(at));
569      let newest = service.with_entry(&id, |entry| {
570        if entry.reads == read {
571          entry.owner = owner;
572          entry.position = (position.unwrap_or_default(), Instant::now());
573          entry.props = Some(props);
574        }
575      });
576      if newest.is_some() {
577        service.publish();
578      }
579    });
580  }
581
582  /// Reads player `id`'s position, which MPRIS doesn't announce.
583  fn read_position(self: &Rc<Self>, connection: &Connection, id: String) {
584    let Some(read) = self.with_entry(&id, |entry| entry.reads) else {
585      return;
586    };
587    let (service, connection) = (Rc::clone(self), connection.clone());
588    self.shared.spawn(async move {
589      let get = (PLAYER, "Position");
590      let call = connection.call_method(Some(id.as_str()), PATH, Some(PROPERTIES), "Get", &get);
591      let reply = service.shared.timeout(CALL_TIMEOUT, call).await;
592      let position = match reply.and_then(|reply| reply.body().deserialize::<OwnedVariant>()) {
593        Ok(position) => micros(&position).unwrap_or_default(),
594        Err(error) => return tracing::info!(id, %error, "can't read a media player's position"),
595      };
596      // A whole read since brings its own.
597      let newest = service.with_entry(&id, |entry| {
598        if entry.reads == read {
599          entry.position = (position, Instant::now());
600        }
601      });
602      if newest.is_some() {
603        service.publish();
604      }
605    });
606  }
607
608  fn on_signal(self: &Rc<Self>, connection: &Connection, message: &Message) {
609    if let Some((name, owner)) = dbus::owner_changed(message) {
610      match (is_player(&name), owner.is_empty()) {
611        (false, _) => {}
612        (true, true) => self.left(&name),
613        (true, false) => self.appeared(connection, name, Some(owner)),
614      }
615      return;
616    }
617    let header = message.header();
618    // Broadcasts from a player's current owner only: any program can
619    // send one from that path, or aim one at this connection.
620    let (Some(sender), None) = (header.sender(), header.destination()) else {
621      return;
622    };
623    if header.path().map(|path| path.as_str()) != Some(PATH) {
624      return;
625    }
626    let ids: Vec<String> = {
627      let state = self.state.borrow();
628      let entries = state.as_ref().map(|state| state.entries.as_slice());
629      let from = entries
630        .unwrap_or_default()
631        .iter()
632        .filter(|entry| entry.owner.as_deref() == Some(sender.as_str()));
633      from.map(|entry| entry.id.clone()).collect()
634    };
635    if let Some((_, interface, changed, dropped)) = dbus::properties_changed(message) {
636      let moved = interface == PLAYER
637        && ["PlaybackStatus", "Rate", "Metadata"]
638          .iter()
639          .any(|name| changed.contains_key(*name));
640      for id in ids {
641        self.with_entry(&id, |entry| {
642          // Where it got to under the old status and rate stands until
643          // the new position is read.
644          if moved && let Some(props) = &entry.props {
645            let (position, at) = entry.position;
646            let now = Instant::now();
647            let player = Player::from_props(entry.id.clone(), props, position, at);
648            entry.position = (player.position_at(now), now);
649          }
650          let Some((root, props)) = &mut entry.props else {
651            return;
652          };
653          let props = match interface.as_str() {
654            ROOT => root,
655            PLAYER => props,
656            _ => return,
657          };
658          dbus::apply(props, changed.clone(), dropped.clone());
659        });
660        // A player that only says a property changed is read whole.
661        if !dropped.is_empty() {
662          self.read(connection, id);
663        } else if moved {
664          self.read_position(connection, id);
665        }
666      }
667      return self.publish();
668    }
669    let member = header.member().map(|member| member.as_str());
670    let seeked = header.interface().map(|interface| interface.as_str()) == Some(PLAYER)
671      && member == Some("Seeked");
672    if let (true, Ok(at)) = (seeked, message.body().deserialize::<i64>()) {
673      let position = micros(&Variant::I64(at)).unwrap_or_default();
674      for id in ids {
675        self.with_entry(&id, |entry| {
676          entry.position = (position, Instant::now());
677          // A position read under way is older than this.
678          entry.reads += 1;
679        });
680      }
681      self.publish();
682    }
683  }
684
685  /// Player `id` left the bus.
686  fn left(&self, id: &str) {
687    if let Some(state) = self.state.borrow_mut().as_mut() {
688      state.entries.retain(|entry| entry.id != id);
689    }
690    self.publish();
691  }
692
693  /// Sets the list from the players read so far, if it changed.
694  fn publish(&self) {
695    // Set apart from the borrow, since effects may ask for more.
696    let shown = self.state.borrow().as_ref().map(|state| {
697      let players = state.entries.iter().filter_map(|entry| {
698        let props = entry.props.as_ref()?;
699        let (position, at) = entry.position;
700        Some(Player::from_props(entry.id.clone(), props, position, at))
701      });
702      (state.players, players.collect::<Vec<_>>())
703    });
704    if let Some((signal, players)) = shown {
705      set_changed(signal, players);
706    }
707  }
708
709  /// Empties the list, once the bus is gone.
710  fn clear(&self) {
711    if let Some(state) = self.state.borrow_mut().as_mut() {
712      state.entries.clear();
713      state.connection = None;
714    }
715    self.publish();
716  }
717}
718
719/// Whether bus name `name` is a player to show.
720fn is_player(name: &str) -> bool {
721  name.starts_with(PREFIX) && name != PLAYERCTLD
722}
723
724/// Joins the session bus at `address` and follows the players, until the
725/// bus closes or the list is disposed.
726async fn run(service: Rc<Service>, address: zbus::Address) {
727  let connection = match zbus::connection::Builder::address(address).build().await {
728    Ok(connection) => connection,
729    Err(error) => {
730      return tracing::info!(%error, "can't reach the session bus, so there are no media players");
731    }
732  };
733  // Made before anything is asked, so no signal is missed.
734  let mut messages = MessageStream::from(&connection);
735  if let Some(state) = service.state.borrow_mut().as_mut() {
736    state.connection = Some(connection.clone());
737  }
738  if let Err(error) = service.join(&connection).await {
739    return tracing::warn!(%error, "couldn't follow the media players on the session bus");
740  }
741  loop {
742    let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
743    if !service.alive() {
744      return service.clear();
745    }
746    match next {
747      Some(Ok(message)) if message.header().message_type() == Kind::Signal => {
748        service.on_signal(&connection, &message);
749      }
750      Some(Ok(_)) => {}
751      Some(Err(error)) => tracing::warn!(%error, "a bad message on the session bus"),
752      None => break,
753    }
754  }
755  tracing::warn!("the session bus closed, so there are no media players");
756  service.clear();
757}