1use crate::{FrameRateError, MediaTime, TICKS_PER_SECOND};
2use core::fmt;
3use serde::{Deserialize, Serialize};
4
5#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
7#[serde(try_from = "Wire")]
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9pub struct FrameRate {
10 num: u32,
11 den: u32,
12}
13#[derive(Deserialize)]
14#[serde(deny_unknown_fields)]
15#[cfg_attr(
16 feature = "schema",
17 derive(schemars::JsonSchema),
18 schemars(rename = "FrameRate")
19)]
20struct Wire {
24 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
26 num: u32,
27 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
29 den: u32,
30}
31impl TryFrom<Wire> for FrameRate {
32 type Error = FrameRateError;
33 fn try_from(wire: Wire) -> Result<Self, Self::Error> {
34 Self::new(wire.num, wire.den)
35 }
36}
37impl FrameRate {
38 pub const fn new(num: u32, den: u32) -> Result<Self, FrameRateError> {
43 if num == 0 {
44 return Err(FrameRateError::ZeroNumerator);
45 }
46 if den == 0 {
47 return Err(FrameRateError::ZeroDenominator);
48 }
49 if (num as u64) > (TICKS_PER_SECOND as u64) * (den as u64) {
50 return Err(FrameRateError::ExceedsTimelineResolution);
51 }
52 let divisor = gcd(num, den);
53 Ok(Self {
54 num: num / divisor,
55 den: den / divisor,
56 })
57 }
58 #[must_use]
60 pub const fn numerator(self) -> u32 {
61 self.num
62 }
63 #[must_use]
65 pub const fn denominator(self) -> u32 {
66 self.den
67 }
68 #[must_use]
70 pub fn frame_start(self, frame: i64) -> Option<MediaTime> {
71 let value = i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(self.den);
72 i64::try_from(value.div_euclid(i128::from(self.num)))
73 .ok()
74 .map(MediaTime)
75 }
76 #[must_use]
78 pub fn frame_at_or_before(self, time: MediaTime) -> Option<i64> {
79 let n = (i128::from(time.0) + 1) * i128::from(self.num);
80 let d = i128::from(TICKS_PER_SECOND) * i128::from(self.den);
81 i64::try_from(-(-n).div_euclid(d) - 1).ok()
82 }
83}
84impl fmt::Display for FrameRate {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "{}/{} fps", self.num, self.den)
87 }
88}
89const fn gcd(mut a: u32, mut b: u32) -> u32 {
90 while b != 0 {
91 let r = a % b;
92 a = b;
93 b = r;
94 }
95 a
96}
97#[cfg(test)]
98mod tests {
99 use super::*;
100 #[test]
101 fn a_frame_lasts_at_least_one_tick() {
102 assert_eq!(
103 FrameRate::new(120_000, 1).map(FrameRate::numerator),
104 Ok(120_000)
105 );
106 assert_eq!(
107 FrameRate::new(240_000, 2).map(|rate| (rate.numerator(), rate.denominator())),
108 Ok((120_000, 1))
109 );
110 for (num, den) in [(120_001, 1), (240_001, 2), (u32::MAX, 35_791)] {
111 assert_eq!(
112 FrameRate::new(num, den),
113 Err(FrameRateError::ExceedsTimelineResolution),
114 "{num}/{den}"
115 );
116 }
117 assert_eq!(
118 FrameRate::new(u32::MAX, 35_792).map(FrameRate::numerator),
119 Ok(u32::MAX)
120 );
121 assert_eq!(FrameRate::new(0, 1), Err(FrameRateError::ZeroNumerator));
122 assert_eq!(FrameRate::new(1, 0), Err(FrameRateError::ZeroDenominator));
123 }
124
125 #[test]
126 fn a_rate_reduces() {
127 let rate = FrameRate::new(60, 2).unwrap();
128 assert_eq!((rate.numerator(), rate.denominator()), (30, 1));
129 assert_eq!(rate, FrameRate::new(30, 1).unwrap());
130 }
131
132 #[test]
133 fn the_wire_form_parses_only_what_new_accepts() {
134 let parse = |json: &str| serde_json::from_str::<FrameRate>(json);
135 assert_eq!(
136 parse(r#"{"num":60000,"den":2002}"#).unwrap(),
137 FrameRate::new(30_000, 1_001).unwrap()
138 );
139 assert_eq!(
140 parse(r#"{"num":120000,"den":1}"#).unwrap(),
141 FrameRate::new(120_000, 1).unwrap()
142 );
143 for (json, message) in [
144 (r#"{"num":120001,"den":1}"#, "exceeds timeline resolution"),
145 (r#"{"num":240001,"den":2}"#, "exceeds timeline resolution"),
146 (r#"{"num":0,"den":1}"#, "numerator must not be zero"),
147 (r#"{"num":30,"den":0}"#, "denominator must not be zero"),
148 (r#"{"num":4294967296,"den":1}"#, "expected u32"),
149 (r#"{"num":30,"den":4294967296}"#, "expected u32"),
150 (r#"{"num":-30,"den":1}"#, "expected u32"),
151 (r#"{"num":30.0,"den":1}"#, "expected u32"),
152 (r#"{"num":30,"den":1,"fps":30}"#, "unknown field `fps`"),
153 (r#"{"num":30}"#, "missing field `den`"),
154 ] {
155 let error = parse(json).expect_err(json).to_string();
156 assert!(error.contains(message), "{json}: {error}");
157 }
158 }
159
160 const RATES: [(u32, u32); 11] = [
162 (30, 1),
163 (30_000, 1_001),
164 (90, 1),
165 (72, 1),
166 (144, 1),
167 (165, 1),
168 (48_000, 1_001),
169 (7, 1),
170 (2_997, 100),
171 (119_999, 1),
172 (120_000, 1),
173 ];
174
175 #[test]
176 fn a_frame_starts_at_the_tick_its_exact_start_falls_in() {
177 let starts = |num, den| {
178 let rate = FrameRate::new(num, den).unwrap();
179 (-1..=3)
180 .map(|frame| rate.frame_start(frame).unwrap().ticks())
181 .collect::<Vec<_>>()
182 };
183 assert_eq!(starts(90, 1), [-1334, 0, 1333, 2666, 4000]);
185 assert_eq!(starts(48_000, 1_001), [-2503, 0, 2502, 5005, 7507]);
187 for (num, den) in RATES {
188 let rate = FrameRate::new(num, den).unwrap();
189 let exact =
190 |frame: i64| i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(den);
191 for frame in -1000..1000 {
192 let start = i128::from(rate.frame_start(frame).unwrap().ticks());
194 assert!(
195 start * i128::from(num) <= exact(frame)
196 && exact(frame) < (start + 1) * i128::from(num),
197 "{rate}: frame {frame} starts at {start}"
198 );
199 }
200 }
201 }
202
203 #[test]
204 fn every_tick_of_a_frame_names_that_frame() {
205 for (num, den) in RATES {
206 let rate = FrameRate::new(num, den).unwrap();
207 for frame in -1000..1000 {
208 let start = rate.frame_start(frame).unwrap();
209 let next = rate.frame_start(frame + 1).unwrap();
210 assert!(start < next, "{rate}: frame {frame} lasts no tick");
211 for tick in [start.0, start.0 + (next.0 - start.0) / 2, next.0 - 1] {
212 assert_eq!(
213 rate.frame_at_or_before(MediaTime(tick)),
214 Some(frame),
215 "{rate}: tick {tick} of frame {frame} [{start}, {next})"
216 );
217 }
218 }
219 }
220 }
221
222 #[test]
223 fn a_frame_starts_only_where_its_start_is_a_tick() {
224 for (num, den) in RATES.into_iter().filter(|&rate| rate != (120_000, 1)) {
225 let rate = FrameRate::new(num, den).unwrap();
226 let last = rate.frame_at_or_before(MediaTime(i64::MAX)).unwrap();
227 let first = rate.frame_at_or_before(MediaTime(i64::MIN)).unwrap();
228 assert!(rate.frame_start(last).is_some(), "{rate}: frame {last}");
229 assert_eq!(rate.frame_start(last + 1), None, "{rate}: frame {last} + 1");
230 assert!(
232 rate.frame_start(first)
233 .is_none_or(|start| start == MediaTime(i64::MIN)),
234 "{rate}: frame {first}"
235 );
236 assert!(
237 rate.frame_start(first + 1).is_some(),
238 "{rate}: frame {first} + 1"
239 );
240 assert_eq!(rate.frame_start(i64::MAX), None, "{rate}");
241 assert_eq!(rate.frame_start(i64::MIN), None, "{rate}");
242 }
243 let fastest = FrameRate::new(120_000, 1).unwrap();
244 assert_eq!(
245 fastest.frame_at_or_before(MediaTime(i64::MAX)),
246 Some(i64::MAX)
247 );
248 assert_eq!(
249 fastest.frame_at_or_before(MediaTime(i64::MIN)),
250 Some(i64::MIN)
251 );
252 assert_eq!(fastest.frame_start(i64::MAX), Some(MediaTime(i64::MAX)));
253 assert_eq!(fastest.frame_start(i64::MIN), Some(MediaTime(i64::MIN)));
254 }
255}