1use std::collections::VecDeque;
7
8use crate::ffi::{AV_NOPTS_VALUE, AVRational};
9
10#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct WallClock {
15 pts: f64,
16 since: Option<f64>,
17 rate: f64,
18}
19
20impl WallClock {
21 pub fn new(pts: f64) -> WallClock {
22 WallClock { pts, since: None, rate: 1.0 }
23 }
24
25 pub fn now(&self, wall: f64) -> f64 {
26 match self.since {
27 Some(s) => self.pts + (wall - s).max(0.0) * self.rate,
28 None => self.pts,
29 }
30 }
31
32 pub fn running(&self) -> bool {
33 self.since.is_some()
34 }
35
36 pub fn start(&mut self, wall: f64) {
37 if self.since.is_none() {
38 self.since = Some(wall);
39 }
40 }
41
42 pub fn pause(&mut self, wall: f64) {
43 self.pts = self.now(wall);
44 self.since = None;
45 }
46
47 pub fn set(&mut self, pts: f64, wall: f64) {
49 self.pts = pts;
50 if self.since.is_some() {
51 self.since = Some(wall);
52 }
53 }
54
55 pub fn set_rate(&mut self, rate: f64, wall: f64) {
56 self.pts = self.now(wall);
57 if self.since.is_some() {
58 self.since = Some(wall);
59 }
60 self.rate = rate;
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct AudioClock {
71 pub pts: f64,
72 pub taken_at: f64,
73 pub span: f64,
74 pub rate: f64,
75}
76
77impl AudioClock {
78 pub fn now(&self, wall: f64) -> f64 {
79 self.pts + ((wall - self.taken_at).max(0.0) * self.rate).min(self.span)
80 }
81}
82
83pub fn needs_tempo(rate: f64) -> bool {
86 (rate - 1.0).abs() >= 1e-4
87}
88
89const ATEMPO_MIN: f64 = 0.5;
92
93pub fn tempo_chain(rate: f64) -> Vec<f64> {
97 let mut left = rate.clamp(0.1, 16.0);
98 let mut chain = Vec::new();
99 while left < ATEMPO_MIN {
100 chain.push(ATEMPO_MIN);
101 left /= ATEMPO_MIN;
102 }
103 chain.push(left);
104 chain
105}
106
107#[derive(Clone, Copy, Debug, PartialEq)]
112pub struct TempoTimeline {
113 rate: f64,
114 sample_rate: f64,
115 start: Option<f64>,
116 frames_out: u64,
117}
118
119impl TempoTimeline {
120 pub fn new(rate: f64, sample_rate: u32) -> TempoTimeline {
121 TempoTimeline { rate, sample_rate: f64::from(sample_rate), start: None, frames_out: 0 }
122 }
123
124 pub fn input(&mut self, pts: Option<f64>) {
126 if self.start.is_none() {
127 self.start = pts;
128 }
129 }
130
131 pub fn next(&self) -> Option<f64> {
133 self.start.map(|s| s + self.frames_out as f64 * self.rate / self.sample_rate)
134 }
135
136 pub fn output(&mut self, frames: usize) {
138 self.frames_out += frames as u64;
139 }
140}
141
142#[derive(Debug)]
144pub struct FrameQueue<T> {
145 items: VecDeque<(f64, T)>,
146}
147
148impl<T> Default for FrameQueue<T> {
149 fn default() -> Self {
150 FrameQueue { items: VecDeque::new() }
151 }
152}
153
154impl<T> FrameQueue<T> {
155 pub fn push(&mut self, pts: f64, item: T) {
156 self.items.push_back((pts, item));
157 }
158
159 pub fn len(&self) -> usize {
160 self.items.len()
161 }
162
163 pub fn is_empty(&self) -> bool {
164 self.items.is_empty()
165 }
166
167 pub fn clear(&mut self) {
168 self.items.clear();
169 }
170
171 pub fn front_pts(&self) -> Option<f64> {
173 self.items.front().map(|(t, _)| *t)
174 }
175
176 pub fn pop_due(&mut self, now: f64) -> Option<(f64, T)> {
179 let mut due = None;
180 while self.items.front().is_some_and(|(t, _)| *t <= now) {
181 due = self.items.pop_front();
182 }
183 due
184 }
185
186 pub fn pop(&mut self) -> Option<(f64, T)> {
189 self.items.pop_front()
190 }
191}
192
193pub fn queue_cap(bytes: usize) -> usize {
196 (QUEUE_BYTES / bytes.max(1)).clamp(2, MAX_QUEUED_FRAMES)
197}
198
199pub const MAX_QUEUED_FRAMES: usize = 8;
200pub const QUEUE_BYTES: usize = 96 << 20;
201
202pub fn secs(ts: i64, tb: AVRational, start: i64) -> Option<f64> {
206 if ts == AV_NOPTS_VALUE || tb.den == 0 {
207 return None;
208 }
209 let start = if start == AV_NOPTS_VALUE { 0 } else { start };
210 Some((ts - start) as f64 * f64::from(tb.num) / f64::from(tb.den))
211}
212
213pub fn seek_ts(t: f64, start: i64) -> i64 {
216 let start = if start == AV_NOPTS_VALUE { 0 } else { start };
217 start + (t.max(0.0) * crate::ffi::AV_TIME_BASE as f64).round() as i64
218}
219
220pub fn before_target(pts: f64, target: f64, fps: f64) -> bool {
225 pts + 1.0 / fps.max(1.0) <= target + 1e-6
226}
227
228pub fn step_back(shown: f64, frames: i32, fps: f64) -> f64 {
230 (shown - f64::from(frames.unsigned_abs()) / fps.max(1.0)).max(0.0)
231}
232
233pub fn turns_from_degrees(ccw: f64) -> u8 {
236 if !ccw.is_finite() {
237 return 0;
238 }
239 ((-ccw / 90.0).round() as i64).rem_euclid(4) as u8
240}
241
242pub fn display_width(w: i32, sar: AVRational) -> i32 {
245 match sar.value() {
246 Some(r) if (r - 1.0).abs() > 1e-3 => ((f64::from(w) * r).round() as i32).max(1),
247 _ => w,
248 }
249}
250
251pub fn fit(w: i32, h: i32, max_side: u32) -> (i32, i32) {
254 let long = w.max(h).max(1) as f64;
255 let max = f64::from(max_side.min(i32::MAX as u32));
256 if long <= max {
257 return (w.max(1), h.max(1));
258 }
259 let s = max / long;
260 (((f64::from(w) * s).round() as i32).max(1), ((f64::from(h) * s).round() as i32).max(1))
261}
262
263pub fn thumbnail_time(duration: f64) -> f64 {
266 if duration > 0.0 { (duration / 3.0).min(10.0) } else { 0.0 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn wall_clock_runs_pauses_and_jumps() {
275 let mut c = WallClock::new(5.0);
276 assert_eq!(c.now(100.0), 5.0, "paused at start");
277 c.start(100.0);
278 assert_eq!(c.now(101.5), 6.5);
279 c.pause(102.0);
280 assert_eq!(c.now(110.0), 7.0, "frozen while paused");
281 c.start(110.0);
282 c.set_rate(2.0, 111.0);
283 assert_eq!(c.now(112.0), 10.0, "one second at 1x then one at 2x");
284 c.set(1.0, 112.0);
285 assert_eq!(c.now(112.5), 2.0);
286 assert!(c.running());
287 }
288
289 #[test]
290 fn audio_clock_stops_at_the_end_of_its_chunk() {
291 let a = AudioClock { pts: 10.0, taken_at: 50.0, span: 0.01, rate: 1.0 };
292 assert_eq!(a.now(50.0), 10.0);
293 assert!((a.now(50.005) - 10.005).abs() < 1e-9);
294 assert_eq!(a.now(51.0), 10.01, "a starved output holds the clock");
295 let fast = AudioClock { rate: 2.0, span: 1.0, ..a };
296 assert_eq!(fast.now(50.25), 10.5);
297 }
298
299 #[test]
300 fn stretched_sound_keeps_the_clock_on_media_time() {
301 let a = AudioClock { pts: 3.0, taken_at: 10.0, span: 1024.0 * 2.0 / 48_000.0, rate: 2.0 };
303 assert!((a.now(10.01) - 3.02).abs() < 1e-9);
304 assert_eq!(a.now(11.0), 3.0 + a.span, "a starved output holds the clock");
305 }
306
307 #[test]
308 fn tempo_is_skipped_at_normal_speed() {
309 assert!(!needs_tempo(1.0));
310 assert!(!needs_tempo(1.00001));
311 assert!(needs_tempo(0.75));
312 assert!(needs_tempo(1.25));
313 }
314
315 #[test]
316 fn tempo_chains_stay_in_the_filters_range() {
317 assert_eq!(tempo_chain(2.0), [2.0]);
318 assert_eq!(tempo_chain(0.5), [0.5]);
319 assert_eq!(tempo_chain(16.0), [16.0]);
320 assert_eq!(tempo_chain(0.25), [0.5, 0.5]);
321 assert_eq!(tempo_chain(100.0), [16.0], "clamped to the player's fastest");
322 for rate in [0.1, 0.13, 0.25, 0.3, 0.49, 0.5, 0.75, 1.25, 3.0, 8.0, 16.0] {
323 let chain = tempo_chain(rate);
324 assert!(chain.iter().all(|t| (0.5..=100.0).contains(t)), "{rate}: {chain:?}");
325 assert!((chain.iter().product::<f64>() - rate).abs() < 1e-9, "{rate}: {chain:?}");
326 }
327 }
328
329 #[test]
330 fn tempo_timeline_counts_from_the_first_sample_in() {
331 let mut t = TempoTimeline::new(2.0, 48_000);
332 assert_eq!(t.next(), None, "nothing known yet");
333 t.input(None);
334 assert_eq!(t.next(), None);
335 t.input(Some(5.0));
336 t.input(Some(5.5));
337 assert_eq!(t.next(), Some(5.0), "later times in say nothing of what comes out");
338 t.output(24_000);
339 assert_eq!(t.next(), Some(6.0), "half a second out at 2x is a second of the clip");
340 let mut slow = TempoTimeline::new(0.25, 48_000);
341 slow.input(Some(0.0));
342 slow.output(48_000);
343 assert_eq!(slow.next(), Some(0.25));
344 }
345
346 #[test]
347 fn due_frames_skip_late_ones() {
348 let mut q = FrameQueue::default();
349 for (i, t) in [0.0, 0.04, 0.08, 0.12].into_iter().enumerate() {
350 q.push(t, i);
351 }
352 assert_eq!(q.pop_due(-0.01), None, "nothing due before the first");
353 assert_eq!(q.pop_due(0.0), Some((0.0, 0)));
354 assert_eq!(q.pop_due(0.1), Some((0.08, 2)), "the frame at 0.04 is skipped");
355 assert_eq!(q.len(), 1);
356 assert_eq!(q.front_pts(), Some(0.12));
357 assert_eq!(q.pop(), Some((0.12, 3)));
358 assert!(q.is_empty());
359 }
360
361 #[test]
362 fn queue_caps_follow_frame_size() {
363 assert_eq!(queue_cap(1920 * 1080 * 4), MAX_QUEUED_FRAMES);
364 assert_eq!(queue_cap(3840 * 2160 * 4), 3);
365 assert_eq!(queue_cap(7680 * 4320 * 4), 2, "never fewer than two");
366 assert_eq!(queue_cap(0), MAX_QUEUED_FRAMES);
367 }
368
369 #[test]
370 fn timestamps_to_seconds() {
371 let tb = AVRational { num: 1, den: 90_000 };
372 assert_eq!(secs(90_000, tb, 0), Some(1.0));
373 assert_eq!(secs(180_000, tb, 90_000), Some(1.0), "measured from the stream start");
374 assert_eq!(secs(45_000, tb, AV_NOPTS_VALUE), Some(0.5));
375 assert_eq!(secs(AV_NOPTS_VALUE, tb, 0), None);
376 assert_eq!(secs(1, AVRational { num: 1, den: 0 }, 0), None);
377 }
378
379 #[test]
380 fn seek_targets() {
381 assert_eq!(seek_ts(1.5, 0), 1_500_000);
382 assert_eq!(seek_ts(1.0, 1_400_000), 2_400_000, "files starting late");
383 assert_eq!(seek_ts(-3.0, AV_NOPTS_VALUE), 0);
384 assert!(before_target(0.9, 1.0, 25.0));
387 assert!(before_target(0.44, 0.48, 25.0), "frame 11 ends where frame 12 starts");
388 assert!(!before_target(0.48, 0.5, 25.0), "frame 12 shows from 0.48 to 0.52");
389 assert!(!before_target(1.0, 1.0, 25.0));
390 assert!((step_back(1.0, 1, 25.0) - 0.96).abs() < 1e-9);
391 assert!((step_back(1.0, -5, 25.0) - 0.8).abs() < 1e-9);
392 assert_eq!(step_back(0.01, 3, 25.0), 0.0);
393 }
394
395 #[test]
396 fn rotations_and_shapes() {
397 assert_eq!(turns_from_degrees(0.0), 0);
398 assert_eq!(turns_from_degrees(-90.0), 1, "a portrait phone clip");
399 assert_eq!(turns_from_degrees(90.0), 3);
400 assert_eq!(turns_from_degrees(180.0), 2);
401 assert_eq!(turns_from_degrees(f64::NAN), 0);
402 assert_eq!(display_width(720, AVRational { num: 32, den: 27 }), 853);
403 assert_eq!(display_width(720, AVRational { num: 1, den: 1 }), 720);
404 assert_eq!(display_width(720, AVRational { num: 0, den: 1 }), 720);
405 assert_eq!(fit(1920, 1080, 320), (320, 180));
406 assert_eq!(fit(100, 50, 320), (100, 50), "never enlarged");
407 }
408
409 #[test]
410 fn thumbnails_are_taken_where_eagle_takes_them() {
411 assert_eq!(thumbnail_time(120.0), 10.0);
412 assert_eq!(thumbnail_time(9.0), 3.0);
413 assert_eq!(thumbnail_time(0.0), 0.0);
414 }
415}