fix: MPRIS reported a frozen position — Position is read on demand

The owner measured QBZ at 4:03 with Plasma's widget stuck at 0:14. The
earlier seek fix was right about seeks and wrong about the shape of the
problem: the position was not drifting, it was FROZEN.

MPRIS `Position` is a read-on-demand property — the spec deliberately keeps
it out of `PropertiesChanged`, so a client reads it when it wants it. The
Linux backend serves whatever it last stored, and it only ever stored a
position when `set_playback` carried one: at a track start (zero) and at a
play/pause edge. Between those, every read returned the same stale number
for as long as the track ran.

This is in the SHARED crate, so the shipping Slint build has it identically —
nobody had watched the widget's clock there.

`MediaIntegration::set_position` stores and emits nothing, so the Qt poll
loop refreshes it on the tick it already computes the position for: one
channel send per second, no bus signal, so it does not become the per-tick
work the perf rules refuse. The method is DEFAULTED, so no other consumer has
to change — `cargo check` passes for qbz-app and qbzd, and the Slint frontend
needs no edit (it cannot be built here, per the qbz-ui wall, so it is
verified statically as R4 requires).

cc
This commit is contained in:
vicrodh committed 2026-08-04 13:51:19 -06:00
1 parent 0372b4d66f
commit f45b703c64
4 files changed
+54

No files matched your search

+13
View File
@@ -54,6 +54,10 @@ enum Update {
position: Option<Time>,
},
Volume(Volume),
/// Position ONLY — stores, emits nothing. MPRIS excludes `Position` from
/// `PropertiesChanged` by design, so there is no signal to send; the point
/// is purely that the next on-demand read is not stale.
Position(Time),
}
/// The cloneable handle returned to the app. Pushing state is a non-blocking
@@ -77,6 +81,12 @@ impl MediaIntegration for LinuxHandle {
fn set_volume(&self, vol: f64) {
let _ = self.tx.try_send(Update::Volume(vol.clamp(0.0, 1.0)));
}
fn set_position(&self, position: std::time::Duration) {
let _ = self
.tx
.try_send(Update::Position(Time::from_micros(position.as_micros() as i64)));
}
}
fn map_status(s: PlaybackStatus) -> MprisStatus {
@@ -284,6 +294,9 @@ async fn apply(server: &Server<QbzMpris>, state: &Arc<Mutex<State>>, update: Upd
state.lock().unwrap().volume = v;
let _ = server.properties_changed([Property::Volume(v)]).await;
}
Update::Position(p) => {
state.lock().unwrap().position = p;
}
}
}
+21
View File
@@ -70,4 +70,25 @@ pub trait MediaIntegration: Send + Sync {
fn set_metadata(&self, meta: &TrackMeta);
fn set_playback(&self, status: PlaybackStatus, position: Option<Duration>);
fn set_volume(&self, vol: f64);
/// Keep the reported playback POSITION current.
///
/// MPRIS `Position` is a read-on-demand property — the spec explicitly
/// excludes it from `PropertiesChanged`, so a client reads it when it
/// wants it and otherwise extrapolates. That makes a stored snapshot the
/// wrong shape unless somebody keeps it fresh: before this existed, the
/// Linux backend only moved its stored position when `set_playback`
/// carried one — at a track start and at a play/pause edge — so a desktop
/// widget would sit on a value minutes stale. Measured 2026-08-04: QBZ at
/// 4:03, Plasma's widget frozen at 0:14.
///
/// Callers push this on their existing playback tick. It stores and emits
/// NOTHING, so it is cheap enough to call once a second and cannot spam
/// the bus.
///
/// Defaulted to a no-op so a backend that has no notion of position (and
/// any future one) needs no change.
fn set_position(&self, position: Duration) {
let _ = position;
}
}
+17
View File
@@ -191,6 +191,23 @@ pub(crate) fn push_playback_state(is_playing: bool, position_secs: u64) {
mc.set_playback(status, Some(Duration::from_secs(position_secs)));
}
/// Keep the reported position current, once per playback tick.
///
/// MPRIS `Position` is READ ON DEMAND — the spec keeps it out of
/// `PropertiesChanged` — so the backend serves whatever it last stored, and
/// before this it only stored one at a track start and at a play/pause edge.
/// A desktop widget therefore showed a value that could be minutes stale:
/// measured 2026-08-04 with QBZ at 4:03 and Plasma's widget stuck at 0:14.
///
/// `set_position` stores and emits nothing, so this is a channel send per
/// second and never a bus signal — it does not become the per-tick work
/// §7-M12 refuses.
pub(crate) fn push_position(position_secs: u64) {
if let Some(mc) = handle() {
mc.set_position(Duration::from_secs(position_secs));
}
}
/// Compare-and-record the metadata de-dupe key. `true` when `key` differs from
/// the last pushed value (→ caller pushes now), recording it as the new
/// last-pushed value. A poisoned lock falls back to pushing.
+3
View File
@@ -2255,6 +2255,9 @@ pub fn start_poll_loop(runtime: Arc<AppRuntime<LoggingAdapter>>) {
cache,
track_id != 0,
);
// The desktop's copy of the same number. Read-on-demand, so it has
// to be kept fresh rather than signalled — see push_position.
crate::media_controls_qt::push_position(position);
// DELIVERED stream params -> the downgrade arrow + tooltip cause.
// Deduped inside, so a steady stream costs no Qt-thread hop.