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