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