Skip to content

Commit 3182204

Browse files
committed
Fix intermittent MPRIS loss
1 parent 971e2e9 commit 3182204

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

‎crates/desktop/src/mpris.rs‎

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const ARTWORK_BUDGET: usize = 8 * 1024 * 1024;
3939
const PROPERTY_CALL_LIMIT: usize = 4;
4040
const ACTION_CALL_LIMIT: usize = 4;
4141
const ACTION_QUEUE_LIMIT: usize = 64;
42+
const MONITOR_RETRY_DELAY: Duration = Duration::from_secs(5);
4243

4344
#[derive(Clone)]
4445
struct Target {
@@ -52,6 +53,11 @@ struct Target {
5253
position_observed_at: Instant,
5354
}
5455

56+
struct DeferredTarget {
57+
owner: String,
58+
retry_at: Option<Instant>,
59+
}
60+
5561
fn player_at(target: &Target, observed_at: Instant) -> MprisPlayer {
5662
let mut player = target.player.clone();
5763
if player.playback_status == PlaybackStatus::Playing {
@@ -77,7 +83,7 @@ pub(super) struct State {
7783
players: HashMap<u32, Target>,
7884
by_owner: HashMap<String, u32>,
7985
by_alias: HashMap<String, u32>,
80-
deferred: HashMap<String, String>,
86+
deferred: HashMap<String, DeferredTarget>,
8187
active: Option<u32>,
8288
artwork_bytes: usize,
8389
property_calls: Arc<Semaphore>,
@@ -178,19 +184,42 @@ impl State {
178184
records
179185
}
180186

181-
fn deferred_candidate(&self, limit: usize) -> Option<(String, String)> {
187+
fn remove_player_for_retry(&mut self, player_id: u32, retry_at: Instant) -> Vec<MprisRecord> {
188+
let Some(target) = self.players.get(&player_id) else {
189+
return Vec::new();
190+
};
191+
let owner = target.owner.clone();
192+
let aliases = target.aliases.iter().cloned().collect::<Vec<_>>();
193+
let records = self.remove_player(player_id);
194+
for alias in aliases {
195+
self.deferred.insert(
196+
alias,
197+
DeferredTarget {
198+
owner: owner.clone(),
199+
retry_at: Some(retry_at),
200+
},
201+
);
202+
}
203+
records
204+
}
205+
206+
fn deferred_candidate(&self, limit: usize, now: Instant) -> Option<(String, String)> {
207+
let ready = |target: &&DeferredTarget| target.retry_at.is_none_or(|at| now >= at);
182208
self.deferred
183209
.iter()
184-
.filter(|(_, owner)| self.by_owner.contains_key(owner.as_str()))
210+
.filter(|(_, target)| {
211+
ready(target) && self.by_owner.contains_key(target.owner.as_str())
212+
})
185213
.min_by_key(|(alias, _)| alias.as_str())
186-
.map(|(alias, owner)| (alias.clone(), owner.clone()))
214+
.map(|(alias, target)| (alias.clone(), target.owner.clone()))
187215
.or_else(|| {
188216
(self.players.len() < limit)
189217
.then(|| {
190218
self.deferred
191219
.iter()
220+
.filter(|(_, target)| ready(target))
192221
.min_by_key(|(alias, _)| alias.as_str())
193-
.map(|(alias, owner)| (alias.clone(), owner.clone()))
222+
.map(|(alias, target)| (alias.clone(), target.owner.clone()))
194223
})
195224
.flatten()
196225
})
@@ -402,7 +431,13 @@ async fn register_alias(
402431
return;
403432
}
404433
if state.players.len() >= player_limit() {
405-
state.deferred.insert(alias, owner);
434+
state.deferred.insert(
435+
alias,
436+
DeferredTarget {
437+
owner,
438+
retry_at: None,
439+
},
440+
);
406441
return;
407442
}
408443
state.deferred.remove(&alias);
@@ -463,7 +498,7 @@ async fn remove_alias(
463498
if state
464499
.deferred
465500
.get(alias)
466-
.is_some_and(|value| value == owner)
501+
.is_some_and(|value| value.owner == owner)
467502
{
468503
state.deferred.remove(alias);
469504
}
@@ -476,7 +511,7 @@ async fn remove_alias(
476511
async fn remove_owner(state: &std::sync::Arc<Mutex<State>>, common: &Common, owner: &str) {
477512
let records = {
478513
let mut state = state.lock().await;
479-
state.deferred.retain(|_, value| value != owner);
514+
state.deferred.retain(|_, value| value.owner != owner);
480515
state
481516
.by_owner
482517
.get(owner)
@@ -493,15 +528,23 @@ async fn remove_monitored_player(
493528
player_id: u32,
494529
owner: &str,
495530
) {
496-
let records = {
531+
let removed = {
497532
let mut state = state.lock().await;
498533
if state.by_owner.get(owner) == Some(&player_id) {
499-
state.remove_player(player_id)
534+
Some(state.remove_player_for_retry(player_id, Instant::now() + MONITOR_RETRY_DELAY))
500535
} else {
501-
Vec::new()
536+
None
502537
}
503538
};
539+
let Some(records) = removed else {
540+
return;
541+
};
504542
send_records(common, records).await;
543+
let capacity_changed = state.lock().await.capacity_changed.clone();
544+
tokio::spawn(async move {
545+
tokio::time::sleep(MONITOR_RETRY_DELAY).await;
546+
capacity_changed.notify_one();
547+
});
505548
}
506549

507550
async fn retry_deferred(
@@ -510,7 +553,10 @@ async fn retry_deferred(
510553
common: &Common,
511554
) {
512555
loop {
513-
let candidate = state.lock().await.deferred_candidate(player_limit());
556+
let candidate = state
557+
.lock()
558+
.await
559+
.deferred_candidate(player_limit(), Instant::now());
514560
let Some((alias, owner)) = candidate else {
515561
return;
516562
};
@@ -1514,18 +1560,46 @@ mod tests {
15141560
let mut state = State::default();
15151561
state.players.insert(1, target(1, now));
15161562
state.by_owner.insert(":1.1".into(), 1);
1517-
state
1518-
.deferred
1519-
.insert("org.mpris.MediaPlayer2.deferred".into(), ":1.2".into());
1563+
state.deferred.insert(
1564+
"org.mpris.MediaPlayer2.deferred".into(),
1565+
DeferredTarget {
1566+
owner: ":1.2".into(),
1567+
retry_at: None,
1568+
},
1569+
);
15201570

1521-
assert_eq!(state.deferred_candidate(1), None);
1571+
assert_eq!(state.deferred_candidate(1, now), None);
15221572
state.remove_player(1);
15231573
assert_eq!(
1524-
state.deferred_candidate(1),
1574+
state.deferred_candidate(1, now),
15251575
Some(("org.mpris.MediaPlayer2.deferred".into(), ":1.2".into()))
15261576
);
15271577
}
15281578

1579+
#[test]
1580+
fn failed_monitor_retries_only_after_its_cooldown() {
1581+
let now = Instant::now();
1582+
let retry_at = now + MONITOR_RETRY_DELAY;
1583+
let alias = "org.mpris.MediaPlayer2.retry".to_string();
1584+
let mut state = State::default();
1585+
let mut player = target(1, now);
1586+
player.aliases.insert(alias.clone());
1587+
state.players.insert(1, player);
1588+
state.by_owner.insert(":1.1".into(), 1);
1589+
state.by_alias.insert(alias.clone(), 1);
1590+
1591+
let records = state.remove_player_for_retry(1, retry_at);
1592+
assert!(matches!(
1593+
records.first(),
1594+
Some(MprisRecord::Delete { player_id: 1 })
1595+
));
1596+
assert_eq!(state.deferred_candidate(1, now), None);
1597+
assert_eq!(
1598+
state.deferred_candidate(1, retry_at),
1599+
Some((alias, ":1.1".into()))
1600+
);
1601+
}
1602+
15291603
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
15301604
async fn property_calls_are_strictly_limited_to_four() {
15311605
let limiter = Arc::new(Semaphore::new(PROPERTY_CALL_LIMIT));

‎crates/server/src/lib.rs‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3871,6 +3871,15 @@ struct ClientState {
38713871
drag_staging_dir: Option<std::path::PathBuf>,
38723872
}
38733873

3874+
#[cfg(target_os = "linux")]
3875+
fn mpris_snapshot_rate_limited(client: &ClientState, enabled: bool, now: Instant) -> bool {
3876+
enabled
3877+
&& client.mpris_subscribed
3878+
&& client
3879+
.last_mpris_snapshot
3880+
.is_some_and(|last| now.duration_since(last) < Duration::from_secs(1))
3881+
}
3882+
38743883
#[cfg(target_os = "linux")]
38753884
fn consume_mpris_action_token(client: &mut ClientState, now: Instant) -> bool {
38763885
const BURST: f64 = 20.0;
@@ -19524,12 +19533,9 @@ async fn handle_client_registered<S: AsyncRead + AsyncWrite + Unpin + Send + 'st
1952419533
}
1952519534
ClientControl::MprisSubscribe { enabled } => {
1952619535
let now = Instant::now();
19527-
let rate_limited = enabled
19528-
&& sess.clients.get(&client_id).is_some_and(|client| {
19529-
client.last_mpris_snapshot.is_some_and(|last| {
19530-
now.duration_since(last) < Duration::from_secs(1)
19531-
})
19532-
});
19536+
let rate_limited = sess.clients.get(&client_id).is_some_and(|client| {
19537+
mpris_snapshot_rate_limited(client, enabled, now)
19538+
});
1953319539
if rate_limited {
1953419540
continue;
1953519541
}
@@ -23773,6 +23779,24 @@ mod tests {
2377323779
));
2377423780
}
2377523781

23782+
#[cfg(target_os = "linux")]
23783+
#[test]
23784+
fn mpris_snapshot_limit_never_suppresses_a_new_subscription() {
23785+
let mut client = test_client();
23786+
let now = Instant::now();
23787+
client.last_mpris_snapshot = Some(now);
23788+
23789+
assert!(!mpris_snapshot_rate_limited(&client, true, now));
23790+
client.mpris_subscribed = true;
23791+
assert!(mpris_snapshot_rate_limited(&client, true, now));
23792+
assert!(!mpris_snapshot_rate_limited(&client, false, now));
23793+
assert!(!mpris_snapshot_rate_limited(
23794+
&client,
23795+
true,
23796+
now + Duration::from_secs(1)
23797+
));
23798+
}
23799+
2377623800
#[cfg(target_os = "linux")]
2377723801
fn mpris_player(position_us: i64, length_us: i64) -> blit_remote::media::MprisPlayer {
2377823802
blit_remote::media::MprisPlayer {

‎docs/design/media-devices-portals.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,9 @@ The bridge calls `GetAll` for `org.mpris.MediaPlayer2` and
738738
`org.freedesktop.DBus.Properties.PropertiesChanged` and `Seeked`. A missing
739739
Player interface, loss of required properties, or three consecutive two-second
740740
D-Bus timeouts removes the player. Optional malformed properties fall back to
741-
their specified defaults without removing it.
741+
their specified defaults without removing it. A removed player whose bus name
742+
remains owned is reconsidered after a five-second cooldown, so a temporarily
743+
unresponsive application can recover without restarting.
742744

743745
Property signals are invalidations, not trusted replacement records. The
744746
bridge batches them into at most one refresh per player per 50 ms and permits
@@ -977,7 +979,8 @@ Rate, 0/1 for Shuffle, and 0 None/1 Track/2 Playlist for LoopStatus. It is zero
977979
for other actions. `track_revision` must match for SetPosition and is zero for
978980
other kinds. A random nonzero nonce correlates one action result.
979981

980-
Fresh snapshot requests are accepted at most once per connection per second.
982+
Fresh snapshot requests are accepted at most once per subscribed connection per
983+
second. A transition from unsubscribed to subscribed is always accepted.
981984
An excess request is ignored while the existing subscription and state stream
982985
continue.
983986

0 commit comments

Comments
 (0)