ff_preview/playback/
clock.rs1use std::time::{Duration, Instant};
9
10enum ClockState {
23 Stopped,
24 Running { started_at: Instant, base: Duration },
25 Paused { frozen_at: Duration },
26}
27
28pub struct PlaybackClock {
53 state: ClockState,
54 rate: f64,
56 seek_offset: Duration,
59}
60
61impl PlaybackClock {
62 #[must_use]
64 pub fn new() -> Self {
65 Self {
66 state: ClockState::Stopped,
67 rate: 1.0,
68 seek_offset: Duration::ZERO,
69 }
70 }
71
72 pub fn start(&mut self) {
80 let base = match &self.state {
81 ClockState::Running { .. } => return,
82 ClockState::Stopped => self.seek_offset,
83 ClockState::Paused { frozen_at } => *frozen_at,
84 };
85 self.state = ClockState::Running {
86 started_at: Instant::now(),
87 base,
88 };
89 }
90
91 pub fn stop(&mut self) {
96 self.state = ClockState::Stopped;
97 self.seek_offset = Duration::ZERO;
98 }
99
100 pub fn pause(&mut self) {
105 if let ClockState::Running { started_at, base } = &self.state {
106 let elapsed = started_at.elapsed().mul_f64(self.rate);
107 self.state = ClockState::Paused {
108 frozen_at: *base + elapsed,
109 };
110 }
111 }
112
113 pub fn resume(&mut self) {
115 if let ClockState::Paused { frozen_at } = self.state {
116 self.state = ClockState::Running {
117 started_at: Instant::now(),
118 base: frozen_at,
119 };
120 }
121 }
122
123 #[must_use]
128 pub fn current_time(&self) -> Duration {
129 match &self.state {
130 ClockState::Stopped => Duration::ZERO,
131 ClockState::Paused { frozen_at } => *frozen_at,
132 ClockState::Running { started_at, base } => {
133 *base + started_at.elapsed().mul_f64(self.rate)
134 }
135 }
136 }
137
138 #[must_use]
144 pub fn current_pts(&self) -> Duration {
145 match &self.state {
146 ClockState::Stopped => self.seek_offset,
147 _ => self.current_time(),
148 }
149 }
150
151 #[must_use]
153 pub fn is_running(&self) -> bool {
154 matches!(self.state, ClockState::Running { .. })
155 }
156
157 pub fn set_rate(&mut self, rate: f64) {
162 if rate <= 0.0 {
163 return;
164 }
165 if let ClockState::Running { started_at, base } = &mut self.state {
166 let elapsed = started_at.elapsed().mul_f64(self.rate);
168 *base += elapsed;
169 *started_at = Instant::now();
170 }
171 self.rate = rate;
172 }
173
174 #[must_use]
176 pub fn rate(&self) -> f64 {
177 self.rate
178 }
179
180 pub fn set_position(&mut self, pts: Duration) {
190 self.seek_offset = pts;
192 if matches!(self.state, ClockState::Running { .. }) {
193 self.state = ClockState::Running {
195 started_at: Instant::now(),
196 base: pts,
197 };
198 } else if matches!(self.state, ClockState::Paused { .. }) {
199 self.state = ClockState::Paused { frozen_at: pts };
200 }
201 }
203}
204
205impl Default for PlaybackClock {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211#[cfg(test)]
214mod tests {
215 use super::*;
216 use std::thread;
217
218 #[test]
219 fn clock_stopped_should_return_zero() {
220 let clock = PlaybackClock::new();
222 assert_eq!(clock.current_time(), Duration::ZERO);
223
224 let mut clock = PlaybackClock::new();
226 clock.start();
227 thread::sleep(Duration::from_millis(5));
228 clock.stop();
229 assert_eq!(
230 clock.current_time(),
231 Duration::ZERO,
232 "current_time() must be ZERO after stop()"
233 );
234 }
235
236 #[test]
237 fn clock_paused_should_freeze_at_pause_time() {
238 let mut clock = PlaybackClock::new();
239 clock.start();
240 thread::sleep(Duration::from_millis(10));
241 clock.pause();
242
243 let t1 = clock.current_time();
244 thread::sleep(Duration::from_millis(10));
245 let t2 = clock.current_time();
246
247 assert_eq!(t1, t2, "current_time() must not advance while paused");
248 assert!(
249 !clock.is_running(),
250 "clock must not report running while paused"
251 );
252 }
253
254 #[test]
255 fn clock_resumed_should_continue_from_pause() {
256 let mut clock = PlaybackClock::new();
257 clock.start();
258 thread::sleep(Duration::from_millis(10));
259 clock.pause();
260 let t_paused = clock.current_time();
261
262 thread::sleep(Duration::from_millis(10));
264 assert_eq!(clock.current_time(), t_paused);
265
266 clock.resume();
267 assert!(clock.is_running());
268 thread::sleep(Duration::from_millis(10));
269
270 let t_after = clock.current_time();
271 assert!(
272 t_after > t_paused,
273 "current_time() must advance after resume(); paused={t_paused:?} after={t_after:?}"
274 );
275 }
276
277 #[test]
278 fn clock_start_should_be_noop_when_already_running() {
279 let mut clock = PlaybackClock::new();
280 clock.start();
281 thread::sleep(Duration::from_millis(10));
282 let t_before = clock.current_time();
283
284 clock.start();
286 let t_after = clock.current_time();
287
288 assert!(
289 t_after >= t_before,
290 "second start() must not reset the clock; before={t_before:?} after={t_after:?}"
291 );
292 }
293
294 #[test]
295 fn clock_resume_should_be_noop_when_not_paused() {
296 let mut clock = PlaybackClock::new();
298 clock.resume();
299 assert!(!clock.is_running());
300 assert_eq!(clock.current_time(), Duration::ZERO);
301
302 clock.start();
304 thread::sleep(Duration::from_millis(5));
305 let t = clock.current_time();
306 clock.resume(); assert!(clock.is_running());
308 assert!(clock.current_time() >= t);
309 }
310
311 #[test]
312 fn clock_default_should_equal_new() {
313 let a = PlaybackClock::new();
314 let b = PlaybackClock::default();
315 assert_eq!(a.current_time(), b.current_time());
316 assert_eq!(a.is_running(), b.is_running());
317 }
318
319 #[test]
320 fn set_rate_should_reject_non_positive_values() {
321 let mut clock = PlaybackClock::new();
322
323 clock.set_rate(0.0);
324 assert!(
325 (clock.rate() - 1.0).abs() < f64::EPSILON,
326 "rate must remain 1.0 after set_rate(0.0)"
327 );
328
329 clock.set_rate(-1.0);
330 assert!(
331 (clock.rate() - 1.0).abs() < f64::EPSILON,
332 "rate must remain 1.0 after set_rate(-1.0)"
333 );
334 }
335
336 #[test]
337 fn set_rate_should_update_rate_when_stopped_or_paused() {
338 let mut clock = PlaybackClock::new();
340 clock.set_rate(0.5);
341 assert!((clock.rate() - 0.5).abs() < f64::EPSILON);
342
343 let mut clock = PlaybackClock::new();
345 clock.start();
346 clock.pause();
347 clock.set_rate(2.0);
348 assert!((clock.rate() - 2.0).abs() < f64::EPSILON);
349 assert!(
350 !clock.is_running(),
351 "clock must remain paused after set_rate"
352 );
353 }
354
355 #[test]
356 fn set_rate_running_should_not_jump_current_time() {
357 let mut clock = PlaybackClock::new();
358 clock.start();
359 thread::sleep(Duration::from_millis(10));
360 let before = clock.current_time();
361 clock.set_rate(2.0);
362 let after = clock.current_time();
363
364 assert!(
365 after >= before,
366 "current_time() must not go backward on set_rate; before={before:?} after={after:?}"
367 );
368 assert!(
369 after - before < Duration::from_millis(20),
370 "current_time() must not jump forward on set_rate; before={before:?} after={after:?}"
371 );
372 assert!((clock.rate() - 2.0).abs() < f64::EPSILON);
373 }
374
375 #[test]
376 #[ignore = "performance thresholds are environment-dependent; run explicitly with -- --include-ignored"]
377 fn rate_two_x_should_advance_at_double_speed() {
378 let mut clock = PlaybackClock::new();
379 clock.set_rate(2.0);
380 clock.start();
381 thread::sleep(Duration::from_millis(50));
382 let elapsed = clock.current_time();
383
384 assert!(
386 elapsed >= Duration::from_millis(80),
387 "2× rate: expected ≥80 ms after 50 ms wall time, got {elapsed:?}"
388 );
389 }
390
391 #[test]
392 fn set_position_should_shift_pts_by_seek_offset() {
393 let seek_target = Duration::from_secs(30);
394
395 let mut clock = PlaybackClock::new();
397 clock.set_position(seek_target);
398 assert_eq!(
399 clock.current_pts(),
400 seek_target,
401 "current_pts() must reflect seek_offset when stopped"
402 );
403
404 clock.start();
406 let pts = clock.current_pts();
407 assert!(
408 pts >= seek_target,
409 "current_pts() must be ≥ seek target after start(); target={seek_target:?} pts={pts:?}"
410 );
411 assert!(
412 clock.is_running(),
413 "clock must be running after set_position + start()"
414 );
415 }
416
417 #[test]
418 fn set_position_while_paused_should_update_frozen_time() {
419 let mut clock = PlaybackClock::new();
420 clock.start();
421 thread::sleep(Duration::from_millis(5));
422 clock.pause();
423
424 let seek_target = Duration::from_secs(10);
425 clock.set_position(seek_target);
426
427 let pts = clock.current_pts();
428 assert_eq!(
429 pts, seek_target,
430 "frozen time must update to seek target; expected={seek_target:?} got={pts:?}"
431 );
432 assert!(
433 !clock.is_running(),
434 "clock must remain paused after set_position"
435 );
436
437 clock.resume();
439 thread::sleep(Duration::from_millis(5));
440 let pts_after = clock.current_pts();
441 assert!(
442 pts_after > seek_target,
443 "current_pts() must advance past seek target after resume(); target={seek_target:?} after={pts_after:?}"
444 );
445 }
446
447 #[test]
448 fn set_position_while_running_should_continue_from_new_position() {
449 let mut clock = PlaybackClock::new();
450 clock.start();
451 thread::sleep(Duration::from_millis(5));
452
453 let seek_target = Duration::from_secs(60);
454 clock.set_position(seek_target);
455
456 let pts = clock.current_pts();
457 assert!(
458 pts >= seek_target,
459 "current_pts() must be ≥ seek target immediately after set_position while running; \
460 target={seek_target:?} pts={pts:?}"
461 );
462 assert!(
463 clock.is_running(),
464 "clock must remain running after set_position"
465 );
466 }
467
468 #[test]
469 fn stop_should_clear_seek_offset() {
470 let mut clock = PlaybackClock::new();
471 clock.set_position(Duration::from_secs(30));
472 clock.stop();
473
474 assert_eq!(
475 clock.current_pts(),
476 Duration::ZERO,
477 "stop() must reset seek_offset to ZERO"
478 );
479 }
480}