1use alpha2_time::{FrameRate, TICKS_PER_SECOND};
9use serde::{Deserialize, Serialize};
10
11use crate::MissingFramePolicy;
12
13const PACING_SLACK_MS: f64 = 0.5;
17
18#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(rename_all = "snake_case")]
21pub enum PlaybackPolicy {
22 #[default]
27 DropVideoKeepAudio,
28 PreserveEveryVideoFrame,
32 SilentScrub,
35 OfflineExact,
38}
39
40impl PlaybackPolicy {
41 pub const ALL: [Self; 4] = [
43 Self::DropVideoKeepAudio,
44 Self::PreserveEveryVideoFrame,
45 Self::SilentScrub,
46 Self::OfflineExact,
47 ];
48
49 #[must_use]
51 pub const fn missing_frames(self) -> MissingFramePolicy {
52 match self {
53 Self::DropVideoKeepAudio | Self::SilentScrub => MissingFramePolicy::HoldPrevious,
54 Self::PreserveEveryVideoFrame | Self::OfflineExact => MissingFramePolicy::Wait,
55 }
56 }
57
58 #[must_use]
60 pub fn audible(self, rate: f64) -> bool {
61 self == Self::DropVideoKeepAudio && (rate - 1.0).abs() < f64::EPSILON
62 }
63
64 #[must_use]
66 pub const fn waits_for_frames(self) -> bool {
67 matches!(self, Self::PreserveEveryVideoFrame | Self::OfflineExact)
68 }
69}
70
71#[derive(Copy, Clone, Debug, Eq, PartialEq)]
73pub struct ClockStep {
74 pub tick: i64,
77 pub audible: bool,
79 pub wait_for_frame: bool,
81}
82
83const AUDIO_STALL_MS: f64 = 250.0;
86
87#[derive(Clone, Debug)]
90pub struct PlaybackClock {
91 policy: PlaybackPolicy,
92 rate: f64,
93 frame_rate: FrameRate,
94 origin_ms: f64,
96 origin_tick: i64,
97 start_tick: i64,
99 following_audio: bool,
101 last_audio: Option<i64>,
102 last_audio_ms: f64,
103 frame: i64,
106 frame_ms: f64,
107}
108
109impl PlaybackClock {
110 #[must_use]
113 pub fn new(
114 policy: PlaybackPolicy,
115 frame_rate: FrameRate,
116 rate: f64,
117 tick: i64,
118 now_ms: f64,
119 ) -> Self {
120 let frame = frame_rate.frame_at_or_before(alpha2_time::MediaTime(tick.max(0)));
121 Self {
122 policy,
123 rate,
124 frame_rate,
125 origin_ms: now_ms,
126 origin_tick: tick,
127 start_tick: tick,
128 following_audio: true,
129 last_audio: None,
130 last_audio_ms: now_ms,
131 frame: frame.unwrap_or(0),
132 frame_ms: now_ms,
133 }
134 }
135
136 #[must_use]
138 pub const fn policy(&self) -> PlaybackPolicy {
139 self.policy
140 }
141
142 pub fn step(&mut self, now_ms: f64, heard: Option<i64>, presented: bool) -> ClockStep {
147 let audible = self.policy.audible(self.rate);
148 match self.policy {
149 PlaybackPolicy::DropVideoKeepAudio | PlaybackPolicy::SilentScrub => {
150 let time = if audible {
151 self.follow_audio(now_ms, heard)
152 } else {
153 self.wall_tick(now_ms)
154 };
155 ClockStep {
156 tick: self.frame_start_at(time),
157 audible,
158 wait_for_frame: false,
159 }
160 }
161 PlaybackPolicy::PreserveEveryVideoFrame | PlaybackPolicy::OfflineExact => {
162 let paced = self.policy == PlaybackPolicy::PreserveEveryVideoFrame;
163 let period = self.frame_ms_at_rate();
164 let due = !paced || now_ms - self.frame_ms >= period - PACING_SLACK_MS;
168 if presented && due {
169 self.frame = (self.frame + if self.rate < 0.0 { -1 } else { 1 }).max(0);
170 self.frame_ms = if paced {
171 (self.frame_ms + period).max(now_ms - period)
172 } else {
173 now_ms
174 };
175 }
176 ClockStep {
177 tick: self.frame_tick(),
178 audible: false,
179 wait_for_frame: true,
180 }
181 }
182 }
183 }
184
185 fn follow_audio(&mut self, now_ms: f64, heard: Option<i64>) -> i64 {
188 let heard = heard.map(|position| position.max(self.start_tick));
191 match heard {
192 Some(position) if Some(position) != self.last_audio => {
193 self.last_audio = Some(position);
194 self.last_audio_ms = now_ms;
195 self.following_audio = true;
196 }
197 Some(position)
198 if self.following_audio && now_ms - self.last_audio_ms > AUDIO_STALL_MS =>
199 {
200 self.following_audio = false;
201 self.origin_ms = now_ms;
202 self.origin_tick = position;
203 }
204 _ => {}
205 }
206 match heard {
207 Some(position) if self.following_audio => position,
208 _ => self.wall_tick(now_ms),
209 }
210 }
211
212 fn wall_tick(&self, now_ms: f64) -> i64 {
213 #[allow(clippy::cast_precision_loss)]
215 let ticks_per_ms = TICKS_PER_SECOND as f64 / 1000.0;
216 let elapsed = (now_ms - self.origin_ms).max(0.0) * ticks_per_ms;
219 #[allow(clippy::cast_possible_truncation)]
221 let advanced = (elapsed * self.rate).floor() as i64;
222 self.origin_tick.saturating_add(advanced)
223 }
224
225 fn frame_start_at(&self, tick: i64) -> i64 {
227 self.frame_rate
228 .frame_at_or_before(alpha2_time::MediaTime(tick.max(0)))
229 .and_then(|frame| self.frame_rate.frame_start(frame))
230 .map_or(0, alpha2_time::MediaTime::ticks)
231 }
232
233 fn frame_tick(&self) -> i64 {
234 self.frame_rate
235 .frame_start(self.frame)
236 .map_or(0, alpha2_time::MediaTime::ticks)
237 }
238
239 fn frame_ms_at_rate(&self) -> f64 {
241 let frame_ms = 1000.0 * f64::from(self.frame_rate.denominator())
242 / f64::from(self.frame_rate.numerator());
243 frame_ms / self.rate.abs().max(f64::MIN_POSITIVE)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn rate() -> FrameRate {
252 FrameRate::new(25, 1).expect("rate")
253 }
254
255 #[test]
256 fn keeping_audio_follows_it_and_falls_back_to_the_wall_clock_when_it_stalls() {
257 let mut clock = PlaybackClock::new(PlaybackPolicy::DropVideoKeepAudio, rate(), 1.0, 0, 0.0);
258 let step = clock.step(10.0, Some(1_200), false);
260 assert_eq!(
261 (step.tick, step.audible, step.wait_for_frame),
262 (0, true, false)
263 );
264 clock.step(100.0, Some(1_200), true);
267 let stalled = clock.step(400.0, Some(1_200), true);
268 assert_eq!(stalled.tick, 0);
269 let later = clock.step(500.0, Some(1_200), true);
271 assert_eq!(later.tick, 2 * 4_800);
272 assert_eq!(clock.step(510.0, Some(50_000), true).tick, 10 * 4_800);
273 let mut fast = PlaybackClock::new(PlaybackPolicy::DropVideoKeepAudio, rate(), 2.0, 0, 0.0);
275 let step = fast.step(1_000.0, Some(7), true);
276 assert_eq!((step.tick, step.audible), (240_000, false));
277 }
278
279 #[test]
280 fn audio_heard_before_the_start_shows_the_start() {
281 let ntsc = FrameRate::new(30_000, 1_001).expect("rate");
284 let mut clock =
285 PlaybackClock::new(PlaybackPolicy::DropVideoKeepAudio, ntsc, 1.0, 8_008, 0.0);
286 assert_eq!(clock.step(5.0, Some(8_007), false).tick, 8_008);
287 assert_eq!(clock.step(10.0, Some(8_007), true).tick, 8_008);
288 assert_eq!(clock.step(20.0, Some(9_000), true).tick, 8_008);
289 let mut stalled =
291 PlaybackClock::new(PlaybackPolicy::DropVideoKeepAudio, ntsc, 1.0, 8_008, 0.0);
292 stalled.step(1.0, Some(8_007), false);
293 assert_eq!(stalled.step(300.0, Some(8_007), true).tick, 8_008);
294 assert_eq!(stalled.step(400.0, Some(8_007), true).tick, 16_016);
296 }
297
298 #[test]
299 fn preserving_every_frame_advances_one_frame_at_a_time_and_waits() {
300 let mut clock =
301 PlaybackClock::new(PlaybackPolicy::PreserveEveryVideoFrame, rate(), 1.0, 0, 0.0);
302 let frame = 4_800; assert_eq!(clock.step(0.0, None, false).tick, 0);
304 assert_eq!(clock.step(20.0, None, true).tick, 0);
306 let next = clock.step(41.0, None, true);
307 assert_eq!(
308 (next.tick, next.audible, next.wait_for_frame),
309 (frame, false, true)
310 );
311 assert_eq!(clock.step(500.0, None, false).tick, frame);
312 assert_eq!(clock.step(501.0, None, true).tick, 2 * frame);
313 }
314
315 #[test]
316 fn preserving_every_frame_keeps_the_rate_on_any_display() {
317 for (num, den) in [
320 (24_000, 1_001),
321 (25, 1),
322 (30_000, 1_001),
323 (30, 1),
324 (60_000, 1_001),
325 (60, 1),
326 ] {
327 let rate = FrameRate::new(num, den).expect("rate");
328 let fps = f64::from(num) / f64::from(den);
329 for hz in [59.94, 60.0, 120.0, 144.0] {
330 for grain in [0.0, 0.1, 1.0] {
331 if fps > hz {
332 continue;
333 }
334 let mut clock = PlaybackClock::new(
335 PlaybackPolicy::PreserveEveryVideoFrame,
336 rate,
337 1.0,
338 0,
339 0.0,
340 );
341 let seconds = 60.0;
342 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
343 let refreshes = (seconds * hz) as u64;
344 let mut tick = 0;
345 for refresh in 0..=refreshes {
346 #[allow(clippy::cast_precision_loss)]
347 let exact = refresh as f64 * 1_000.0 / hz;
348 let now = if grain > 0.0 {
349 (exact / grain).round() * grain
350 } else {
351 exact
352 };
353 tick = clock.step(now, None, true).tick;
354 }
355 let shown = clock.frame;
356 let expected = seconds * fps;
357 #[allow(clippy::cast_precision_loss)]
358 let short = expected - shown as f64;
359 assert!(
360 short.abs() <= 1.0,
361 "{fps} fps at {hz} Hz, {grain} ms: {shown} frames in {seconds} s, not {expected} (tick {tick})"
362 );
363 }
364 }
365 }
366 }
367
368 #[test]
369 fn offline_exact_moves_as_soon_as_each_frame_is_shown() {
370 let mut clock = PlaybackClock::new(PlaybackPolicy::OfflineExact, rate(), 1.0, 9_600, 0.0);
371 assert_eq!(clock.step(0.0, None, false).tick, 9_600);
372 assert_eq!(clock.step(0.0, None, true).tick, 14_400);
373 assert_eq!(
374 PlaybackPolicy::OfflineExact.missing_frames(),
375 MissingFramePolicy::Wait
376 );
377 assert_eq!(
378 PlaybackPolicy::SilentScrub.missing_frames(),
379 MissingFramePolicy::HoldPrevious
380 );
381 }
382
383 #[test]
384 fn the_wall_clock_holds_at_its_start_until_its_start_time() {
385 let mut clock = PlaybackClock::new(PlaybackPolicy::SilentScrub, rate(), 1.0, 0, 1_000.0);
386 assert_eq!(clock.step(992.0, None, false).tick, 0);
387 assert_eq!(clock.step(1_500.0, None, true).tick, 12 * 4_800);
389 let mut silent = PlaybackClock::new(
390 PlaybackPolicy::DropVideoKeepAudio,
391 rate(),
392 1.0,
393 9_600,
394 1_000.0,
395 );
396 assert_eq!(silent.step(990.0, None, false).tick, 9_600);
397 }
398
399 #[test]
400 fn scrubbing_is_silent_and_follows_the_wall_clock_backwards_too() {
401 let mut clock = PlaybackClock::new(PlaybackPolicy::SilentScrub, rate(), -1.0, 120_000, 0.0);
402 let step = clock.step(500.0, Some(0), true);
403 assert_eq!((step.tick, step.audible), (12 * 4_800, false));
404 }
405
406 #[test]
407 fn audio_positions_inside_one_frame_ask_for_that_frame_once() {
408 let mut clock = PlaybackClock::new(PlaybackPolicy::DropVideoKeepAudio, rate(), 1.0, 0, 0.0);
409 let ticks: Vec<i64> = [(16.0, 4_800), (32.0, 6_400), (48.0, 9_599)]
411 .into_iter()
412 .map(|(now, heard)| clock.step(now, Some(heard), true).tick)
413 .collect();
414 assert_eq!(ticks, [4_800; 3]);
415 assert_eq!(clock.step(64.0, Some(9_600), true).tick, 9_600);
417 }
418
419 #[test]
420 fn the_wall_clock_after_an_audio_stall_is_quantized_too() {
421 let mut clock = PlaybackClock::new(PlaybackPolicy::DropVideoKeepAudio, rate(), 1.0, 0, 0.0);
422 clock.step(0.0, Some(100), false);
423 assert_eq!(clock.step(300.0, Some(100), true).tick, 0);
425 assert_eq!(clock.step(330.0, Some(100), true).tick, 0);
427 assert_eq!(clock.step(340.0, Some(100), true).tick, 4_800);
428 }
429
430 #[test]
431 fn reverse_play_asks_for_the_frame_containing_the_time() {
432 let mut clock = PlaybackClock::new(PlaybackPolicy::SilentScrub, rate(), -1.0, 122_200, 0.0);
435 assert_eq!(clock.step(10.0, None, true).tick, 120_000);
436 assert_eq!(clock.step(40.0, None, true).tick, 115_200);
437 assert_eq!(clock.step(10_000.0, None, true).tick, 0);
439 }
440}