fix: emit MPRIS Seeked, so an already-open widget stops extrapolating
The owner's observation is what separated the two halves of this: "el contador del seekbar del widget solo empieza a contar hasta que lo abro." A client reads Position ONCE, when it opens, and extrapolates from there — it does not poll. So two different things were broken and only one was fixed by keeping the stored position fresh. Fresh storage fixes what a client sees when it OPENS. `Seeked` is the only thing that reaches one that is ALREADY open: it tells the client to stop extrapolating and re-read. Without it, a seek moved the music and the widget kept counting from where it had been. So the seek path emits Seeked instead of re-asserting the playback state, which was the previous commit's guess at a cure inside an API that had no signal. `mpris_server::Signal::Seeked` was there all along. Defaulted on the trait to `set_position`, so a backend without the signal still gets the value right on the next read and no other consumer changes: cargo check passes for qbz-app and qbzd. cc
This commit is contained in:
4 files changed
+48
-15
No files matched your search
@@ -20,7 +20,7 @@ use std::sync::{
|
||||
use mpris_server::zbus::{self, fdo};
|
||||
use mpris_server::{
|
||||
LoopStatus, Metadata, PlaybackRate, PlaybackStatus as MprisStatus, PlayerInterface, Property,
|
||||
RootInterface, Server, Time, TrackId, Volume,
|
||||
RootInterface, Server, Signal, Time, TrackId, Volume,
|
||||
};
|
||||
|
||||
use crate::inhibit::SleepInhibitor;
|
||||
@@ -58,6 +58,11 @@ enum Update {
|
||||
/// `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),
|
||||
/// A DISCONTINUOUS jump. Stores like `Position` and additionally emits the
|
||||
/// `Seeked` signal, which is the ONE thing that tells an already-open
|
||||
/// client to stop extrapolating and re-read. Without it a widget keeps
|
||||
/// counting from wherever it was until it happens to poll again.
|
||||
Seeked(Time),
|
||||
}
|
||||
|
||||
/// The cloneable handle returned to the app. Pushing state is a non-blocking
|
||||
@@ -87,6 +92,12 @@ impl MediaIntegration for LinuxHandle {
|
||||
.tx
|
||||
.try_send(Update::Position(Time::from_micros(position.as_micros() as i64)));
|
||||
}
|
||||
|
||||
fn seeked(&self, position: std::time::Duration) {
|
||||
let _ = self
|
||||
.tx
|
||||
.try_send(Update::Seeked(Time::from_micros(position.as_micros() as i64)));
|
||||
}
|
||||
}
|
||||
|
||||
fn map_status(s: PlaybackStatus) -> MprisStatus {
|
||||
@@ -297,6 +308,10 @@ async fn apply(server: &Server<QbzMpris>, state: &Arc<Mutex<State>>, update: Upd
|
||||
Update::Position(p) => {
|
||||
state.lock().unwrap().position = p;
|
||||
}
|
||||
Update::Seeked(p) => {
|
||||
state.lock().unwrap().position = p;
|
||||
let _ = server.emit(Signal::Seeked { position: p }).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,4 +91,18 @@ pub trait MediaIntegration: Send + Sync {
|
||||
fn set_position(&self, position: Duration) {
|
||||
let _ = position;
|
||||
}
|
||||
|
||||
/// Report a DISCONTINUOUS jump — a user seek, not the ordinary advance of
|
||||
/// playback.
|
||||
///
|
||||
/// This is the one thing that reaches a client which is already open and
|
||||
/// extrapolating: `Seeked` tells it to stop and re-read. Keeping the
|
||||
/// stored position fresh (above) fixes what a client sees when it OPENS;
|
||||
/// this fixes what it sees while it is already watching.
|
||||
///
|
||||
/// Defaulted to `set_position`, so a backend without the signal still gets
|
||||
/// the value right on the next read.
|
||||
fn seeked(&self, position: Duration) {
|
||||
self.set_position(position);
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,20 @@ pub(crate) fn push_position(position_secs: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Report a user SEEK — a discontinuous jump, not the ordinary advance.
|
||||
///
|
||||
/// `push_position` fixes what a client sees when it OPENS; this is what
|
||||
/// reaches one that is ALREADY open. The owner's observation is what made the
|
||||
/// distinction concrete: *"el contador del seekbar del widget solo empieza a
|
||||
/// contar hasta que lo abro"* — the widget extrapolates from the value it read
|
||||
/// at open and never looks again, so only the `Seeked` signal can tell it the
|
||||
/// ground moved.
|
||||
pub(crate) fn push_seeked(position_secs: u64) {
|
||||
if let Some(mc) = handle() {
|
||||
mc.seeked(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.
|
||||
|
||||
@@ -1619,20 +1619,10 @@ pub async fn seek_frac(runtime: &Arc<AppRuntime<LoggingAdapter>>, frac: f32) {
|
||||
log::warn!("[qbz-qt] seek failed: {e}");
|
||||
return;
|
||||
}
|
||||
// Tell the desktop where we jumped to. Without this the MPRIS position is
|
||||
// only ever refreshed on the play/pause edge, so a widget extrapolates
|
||||
// from a base that is now wrong by the whole seek distance — the owner
|
||||
// measured ~40 s of lag on 2026-08-04 after seeking forward.
|
||||
//
|
||||
// The shared crate exposes no `Seeked` emitter (`types.rs`), and
|
||||
// `set_playback` already carries a position, so the honest fix inside the
|
||||
// current API is to re-assert the state at the new position. The SHIPPING
|
||||
// Slint build has the same gap and the same cure would apply there — that
|
||||
// is a shared-crate conversation, not this one.
|
||||
crate::media_controls_qt::push_playback_state(
|
||||
runtime.core().player().get_playback_event().is_playing,
|
||||
target,
|
||||
);
|
||||
// Tell the desktop we JUMPED. The 1 Hz `push_position` keeps the stored
|
||||
// value fresh for a client that opens later; `Seeked` is the only thing
|
||||
// that reaches one already open and extrapolating from what it read then.
|
||||
crate::media_controls_qt::push_seeked(target);
|
||||
}
|
||||
|
||||
pub async fn set_volume(runtime: &Arc<AppRuntime<LoggingAdapter>>, volume: f32) {
|
||||
|
||||
Reference in new issue
Block a user