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:
4 files changed
+54
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user