Skip to main content

falcon_ffmpeg/
sync.rs

1//! The player's timing rules, free of FFmpeg so they can be tested: the
2//! wall clock, the audio clock, the queue of decoded frames waiting to be
3//! shown, the arithmetic of timestamps and seeks, and the bookkeeping of
4//! sound stretched to another speed.
5
6use std::collections::VecDeque;
7
8use crate::ffi::{AV_NOPTS_VALUE, AVRational};
9
10/// A media clock running off the wall clock: `pts` at wall time `since`,
11/// advancing at `rate` while running and frozen while paused. Times are
12/// seconds; the wall time is passed in, so the clock is plain arithmetic.
13#[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    /// Jumps to `pts`, keeping running or paused.
48    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/// Where the sound is: the media time of the first sample of the chunk
65/// the output last took, when it took it, and how much media time that
66/// chunk covers (at speed `rate`, each second of samples covers `rate`
67/// seconds of the clip, whose pitch stays put). Between takes the clock runs on from the chunk's start,
68/// but never past its end, so a starved output stops the clock.
69#[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
83/// Whether sound at `rate` needs stretching. At normal speed (to within
84/// the player's own tolerance) it skips the filter altogether.
85pub fn needs_tempo(rate: f64) -> bool {
86    (rate - 1.0).abs() >= 1e-4
87}
88
89/// The slowest speed one `atempo` filter takes; it goes up to 100, past the
90/// player's fastest.
91const ATEMPO_MIN: f64 = 0.5;
92
93/// The `atempo` speeds to chain for `rate` (clamped to the player's 0.1 to
94/// 16): one filter, with halving filters ahead of it below 0.5, as FFmpeg's
95/// documentation describes. Their product is `rate`.
96pub 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/// The media time of sound coming out of a tempo filter, which holds some
108/// back: every sample out covers `rate` samples in, counted from the time
109/// of the first sample in, so the timestamps on what goes in don't apply to
110/// what comes out of the same call.
111#[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    /// Notes the time of sound going in; only the first known one counts.
125    pub fn input(&mut self, pts: Option<f64>) {
126        if self.start.is_none() {
127            self.start = pts;
128        }
129    }
130
131    /// The media time of the next sample frame out, once one is known.
132    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    /// Notes `frames` sample frames coming out.
137    pub fn output(&mut self, frames: usize) {
138        self.frames_out += frames as u64;
139    }
140}
141
142/// Decoded frames waiting for their time, oldest first.
143#[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    /// The time of the next frame to show.
172    pub fn front_pts(&self) -> Option<f64> {
173        self.items.front().map(|(t, _)| *t)
174    }
175
176    /// The newest frame due at `now`, dropping older due frames: when the
177    /// display falls behind, frames are skipped rather than shown late.
178    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    /// The next frame whatever its time (for stepping and the first frame
187    /// after a seek).
188    pub fn pop(&mut self) -> Option<(f64, T)> {
189        self.items.pop_front()
190    }
191}
192
193/// How many decoded frames of `bytes` each to keep ready: up to
194/// [`MAX_QUEUED_FRAMES`], within [`QUEUE_BYTES`], and never fewer than two.
195pub 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
202/// Seconds of a timestamp in `tb`, measured from the stream's `start`
203/// (also in `tb`; files that don't start at zero, such as MPEG-TS
204/// captures, show from 0). `None` for a missing timestamp.
205pub 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
213/// The container timestamp (`AV_TIME_BASE` units, as `av_seek_frame` takes
214/// with stream -1) of `t` seconds after the file's `start`.
215pub 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
220/// Whether a frame at `pts` is still before a seek's `target` and should be
221/// decoded but not shown: the next frame (one frame time later) would
222/// already be showing at `target`. The decoder starts at the keyframe
223/// before the target and works forward to the frame on screen at it.
224pub fn before_target(pts: f64, target: f64, fps: f64) -> bool {
225    pts + 1.0 / fps.max(1.0) <= target + 1e-6
226}
227
228/// The time to seek to for a step of `frames` back from `shown`.
229pub 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
233/// Clockwise quarter turns (0 to 3) for a display matrix rotation, which
234/// FFmpeg gives in degrees anticlockwise.
235pub 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
242/// The shown width of a picture `w` pixels wide with sample aspect ratio
243/// `sar` (anamorphic DVD video has non-square pixels).
244pub 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
251/// The size to scale a `w` × `h` picture to so its long side is at most
252/// `max_side`, never enlarging.
253pub 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
263/// Where Eagle's thumbnailer takes a video's picture: ten seconds in, or a
264/// third of the way through a clip shorter than thirty seconds.
265pub 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        // A second of output at double speed covers two seconds of the clip.
302        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        // Decoding forward from the keyframe: frames that are over by the
385        // target are dropped; the one on screen at it is kept.
386        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}