1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
use std::time::{Duration, Instant};
use super::{DBusError, LoopStatus, PlaybackStatus};
use extensions::DurationExtensions;
use metadata::Metadata;
use player::Player;
/// Struct containing information about current progress of a Player.
///
/// It has access to the metadata of the current track, as well as information about the current
/// position of the track.
///
/// It is up to you to decide on how outdated information you want to rely on when implementing
/// progress rendering.
#[derive(Debug)]
pub struct Progress {
metadata: Metadata,
playback_status: PlaybackStatus,
shuffle: bool,
loop_status: LoopStatus,
/// When this Progress was constructed, in order to calculate how old it is.
instant: Instant,
position: Duration,
rate: f64,
current_volume: f64,
}
/// Controller for calculating Progress for a given Player.
///
/// Call the `tick` method to get the most current Progress data.
#[derive(Debug)]
pub struct ProgressTracker<'a> {
player: &'a Player<'a>,
interval: Duration,
last_tick: Instant,
last_progress: Progress,
}
impl<'a> ProgressTracker<'a> {
/// Construct a new ProgressTracker for the provided Player.
///
/// The `interval_ms` value is the desired time between ticks when calling the `tick` method.
/// See `tick` for more information about that.
///
/// You probably want to use `Player::track_progress` instead of this method.
///
/// # Errors
///
/// Returns an error in case Player metadata or state retrieval over DBus fails.
pub fn new(player: &'a Player<'a>, interval_ms: u32) -> Result<Self, DBusError> {
Ok(ProgressTracker {
player: player,
interval: Duration::from_millis(u64::from(interval_ms)),
last_tick: Instant::now(),
last_progress: Progress::from_player(player)?,
})
}
/// Returns a (`Progress`, `bool`) pair at each interval, or as close to each interval as
/// possible.
///
/// The `bool` is `true` if the `Progress` was refreshed, or `false` if the old `Progress` was
/// reused.
///
/// If there is time left until the next interval window, then the tracker will process DBus
/// events to determine if something changed (and potentially perform a full refresh). If there
/// is no time left, then the previous `Progress` will be returned again.
///
/// If refreshing failed for some reason the old `Progress` will be returned.
///
/// It is recommended to call this inside a loop to maintain your progress display.
///
/// ## On reusing `Progress` instances
///
/// `Progress` can be reused until something about the player changes, like track or playback
/// status. As long as nothing changes, `Progress` can accurately determine playback position
/// from timing data.
///
/// You can use the returned `bool` in order to perform similar optimizations yourself, as a
/// `false` value means that nothing (except potentially `position`) changed.
///
/// # Examples
///
/// Simple progress tracker:
///
/// ```rust,no_run
/// # use mpris::{PlayerFinder, Metadata, PlaybackStatus, Progress};
/// # use std::time::Duration;
/// # fn update_progress_bar(_: Duration) { }
/// # let player = PlayerFinder::new().unwrap().find_active().unwrap();
/// #
/// // Refresh every 100ms
/// let mut progress_tracker = player.track_progress(100).unwrap();
/// loop {
/// let (progress, _) = progress_tracker.tick();
/// update_progress_bar(progress.position());
/// }
/// ```
///
/// Using the `was_refreshed` `bool`:
///
/// ```rust,no_run
/// # use mpris::PlayerFinder;
/// # use std::time::Duration;
/// # fn update_progress_bar(_: Duration) { }
/// # fn reset_progress_bar(_: Duration, _: Option<Duration>) { }
/// # fn update_track_title(_: Option<&str>) { }
/// #
/// # let player = PlayerFinder::new().unwrap().find_active().unwrap();
/// #
/// // Refresh every 100ms
/// let mut progress_tracker = player.track_progress(100).unwrap();
/// loop {
/// let (progress, was_changed) = progress_tracker.tick();
/// if was_changed {
/// update_track_title(progress.metadata().title());
/// reset_progress_bar(progress.position(), progress.length());
/// } else {
/// update_progress_bar(progress.position());
/// }
/// }
/// ```
pub fn tick(&mut self) -> (&Progress, bool) {
let mut did_refresh = false;
// Calculate time left until we're expected to return with new data.
let time_left = self.interval
.checked_sub(self.last_tick.elapsed())
.unwrap_or_else(|| Duration::from_millis(0));
// Refresh events if we're not late.
if time_left > Duration::from_millis(0) {
self.player.connection().process_events_blocking(time_left);
}
// If we got a new event since the last time we ticked, then reload fresh data.
if self.player
.connection()
.is_bus_updated_after(self.player.unique_name(), &self.last_tick)
{
did_refresh = self.refresh();
}
(self.progress(), did_refresh)
}
/// Force a refresh right now.
///
/// This will ignore the interval and perform a refresh anyway, storing the result as the last
/// `Progress` value.
///
/// # Errors
///
/// Returns an error if the refresh failed.
pub fn force_refresh(&mut self) -> Result<(), DBusError> {
Progress::from_player(self.player).map(|progress| {
self.last_progress = progress;
})
}
fn progress(&mut self) -> &Progress {
self.last_tick = Instant::now();
&self.last_progress
}
fn refresh(&mut self) -> bool {
if let Ok(progress) = Progress::from_player(self.player) {
self.last_progress = progress;
return true;
}
false
}
}
impl Progress {
pub(crate) fn from_player<'a>(player: &'a Player<'a>) -> Result<Progress, DBusError> {
Ok(Progress {
metadata: player.get_metadata()?,
playback_status: player.get_playback_status()?,
shuffle: player.get_shuffle()?,
loop_status: player.get_loop_status()?,
rate: player.get_playback_rate()?,
position: player.get_position()?,
current_volume: player.get_volume()?,
instant: Instant::now(),
})
}
/// The track metadata at the point in time that this Progress was constructed.
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
/// The playback status at the point in time that this Progress was constructed.
pub fn playback_status(&self) -> PlaybackStatus {
self.playback_status
}
/// The shuffle status at the point in time that this Progress was constructed.
pub fn shuffle(&self) -> bool {
self.shuffle
}
/// The loop status at the point in time that this Progress was constructed.
pub fn loop_status(&self) -> LoopStatus {
self.loop_status
}
/// The playback rate at the point in time that this Progress was constructed.
pub fn playback_rate(&self) -> f64 {
self.rate
}
/// Returns the length of the current track as a `Duration`.
pub fn length(&self) -> Option<Duration> {
self.metadata.length()
}
/// Returns the current position of the current track as a `Duration`.
///
/// This method will calculate the expected position of the track at the instant of the
/// invocation using the `initial_position` and knowledge of how long ago that position was
/// determined.
///
/// **Note:** Some players might not support this and will return a bad position. Spotify is
/// one such example. There is no reliable way of detecting problematic players, so it will be
/// up to your client to check for this.
///
/// One way of doing this is to query the `initial_position` for two measures with the
/// `Playing` `PlaybackStatus` and if both are `0`, then it is likely that this client does not
/// support positions.
pub fn position(&self) -> Duration {
self.position + self.elapsed()
}
/// Returns the position that the current track was at when the `Progress` was created.
///
/// This is the number that was returned for the `Position` property in the MPRIS2 interface.
pub fn initial_position(&self) -> Duration {
self.position.clone()
}
/// The instant where this `Progress` was recorded.
///
/// See: `age`.
pub fn created_at(&self) -> &Instant {
&self.instant
}
/// Returns the age of the data as a `Duration`.
///
/// If the `Progress` has a high age it is more likely to be out of date.
pub fn age(&self) -> Duration {
self.instant.elapsed()
}
/// Returns the player's volume as it was at the time of refresh.
///
/// See: `Player::get_volume`.
pub fn current_volume(&self) -> f64 {
self.current_volume
}
fn elapsed(&self) -> Duration {
let elapsed_ms = match self.playback_status {
PlaybackStatus::Playing => DurationExtensions::as_millis(&self.age()) as f64 * self.rate,
_ => 0.0,
};
Duration::from_millis(elapsed_ms as u64)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn it_progresses_position_when_playing_at_microseconds() {
let progress = Progress {
metadata: Metadata::new(String::from("id")),
playback_status: PlaybackStatus::Playing,
shuffle: false,
loop_status: LoopStatus::None,
rate: 1.0,
position: Duration::from_micros_ext(1),
current_volume: 0.0,
instant: Instant::now(),
};
assert_eq!(progress.initial_position(), Duration::from_micros_ext(1));
assert!(progress.position() >= progress.initial_position());
}
#[test]
fn it_does_not_progress_when_paused() {
let progress = Progress {
metadata: Metadata::new(String::from("id")),
playback_status: PlaybackStatus::Paused,
shuffle: false,
loop_status: LoopStatus::None,
rate: 1.0,
position: Duration::from_micros_ext(1336),
current_volume: 0.0,
instant: Instant::now() - Duration::from_millis(500),
};
assert_eq!(progress.position(), progress.initial_position());
}
}