1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Serialize, Serializer};
7
8use crate::{Error, Result};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct FrameRate {
13 num: u32,
14 den: u32,
15}
16
17const NTSC_BASES: [u32; 6] = [24, 30, 48, 60, 120, 240];
19
20impl FrameRate {
21 pub fn new(num: u32, den: u32) -> Result<Self> {
22 if num == 0 || den == 0 {
23 return Err(Error::InvalidFrameRate(format!("{num}/{den}")));
24 }
25 let g = gcd(num, den);
26 let rate = FrameRate {
27 num: num / g,
28 den: den / g,
29 };
30 if !(1.0..=480.0).contains(&rate.as_f64()) {
31 return Err(Error::InvalidFrameRate(format!(
32 "{num}/{den} (must be between 1 and 480 fps)"
33 )));
34 }
35 Ok(rate)
36 }
37
38 pub fn num(self) -> u32 {
39 self.num
40 }
41
42 pub fn den(self) -> u32 {
43 self.den
44 }
45
46 pub fn as_f64(self) -> f64 {
47 f64::from(self.num) / f64::from(self.den)
48 }
49
50 pub fn frame_duration(self) -> f64 {
52 f64::from(self.den) / f64::from(self.num)
53 }
54
55 pub fn frame_time(self, frame: i64) -> f64 {
57 frame as f64 * self.frame_duration()
58 }
59
60 pub fn nominal(self) -> u32 {
62 ((self.num + self.den / 2) / self.den).max(1)
63 }
64
65 pub fn is_drop_frame(self) -> bool {
67 self.den == 1001 && (self.num == 30000 || self.num == 60000)
68 }
69
70 pub fn samples_per_frame(self, sample_rate: u32) -> f64 {
72 f64::from(sample_rate) * self.frame_duration()
73 }
74
75 pub fn frame_start_sample(self, frame: i64, sample_rate: u32) -> i64 {
77 let n = i128::from(frame) * i128::from(sample_rate) * i128::from(self.den);
78 let d = i128::from(self.num);
79 (-(-n).div_euclid(d)) as i64
81 }
82
83 pub fn frames_in(self, seconds: f64) -> u64 {
85 ((seconds * self.as_f64()).round() as u64).max(1)
86 }
87
88 pub fn label(self) -> String {
90 if self.den == 1 {
91 return self.num.to_string();
92 }
93 let s = format!("{:.3}", self.as_f64());
94 s.trim_end_matches('0').trim_end_matches('.').to_string()
95 }
96}
97
98impl fmt::Display for FrameRate {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 if self.den == 1 {
101 write!(f, "{}", self.num)
102 } else {
103 write!(f, "{}/{}", self.num, self.den)
104 }
105 }
106}
107
108impl FromStr for FrameRate {
109 type Err = Error;
110
111 fn from_str(s: &str) -> Result<Self> {
112 let s = s.trim();
113 let bad = || Error::InvalidFrameRate(s.to_string());
114 if let Some((n, d)) = s.split_once('/') {
115 let n = n.trim().parse().map_err(|_| bad())?;
116 let d = d.trim().parse().map_err(|_| bad())?;
117 return FrameRate::new(n, d);
118 }
119 let v: f64 = s.parse().map_err(|_| bad())?;
120 if !v.is_finite() || v <= 0.0 {
121 return Err(bad());
122 }
123 for base in NTSC_BASES {
124 let ntsc = f64::from(base) * 1000.0 / 1001.0;
125 if (v - ntsc).abs() < 0.006 {
126 return FrameRate::new(base * 1000, 1001);
127 }
128 }
129 if (v - v.round()).abs() < 1e-9 {
130 return FrameRate::new(v.round() as u32, 1);
131 }
132 FrameRate::new((v * 1000.0).round() as u32, 1000)
133 }
134}
135
136impl Serialize for FrameRate {
137 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
138 serializer.collect_str(self)
139 }
140}
141
142#[cfg(feature = "ffmpeg")]
143impl From<FrameRate> for ffmpeg_next::Rational {
144 fn from(rate: FrameRate) -> Self {
145 ffmpeg_next::Rational::new(rate.num as i32, rate.den as i32)
146 }
147}
148
149fn gcd(mut a: u32, mut b: u32) -> u32 {
150 while b != 0 {
151 (a, b) = (b, a % b);
152 }
153 a
154}
155
156pub fn parse_time(input: &str) -> Result<f64> {
161 let s = input.trim();
162 let bad = || Error::InvalidTime(input.to_string());
163 let (sign, body) = match s.as_bytes().first() {
164 Some(b'-') => (-1.0, &s[1..]),
165 Some(b'+') => (1.0, &s[1..]),
166 _ => (1.0, s),
167 };
168 let body = body.trim();
169 let number = |t: &str| -> Result<f64> {
170 let v: f64 = t.trim().parse().map_err(|_| bad())?;
171 if v.is_finite() && v >= 0.0 {
172 Ok(v)
173 } else {
174 Err(bad())
175 }
176 };
177 let seconds = if let Some(ms) = body.strip_suffix("ms") {
178 number(ms)? / 1000.0
179 } else if let Some(sec) = body.strip_suffix('s') {
180 number(sec)?
181 } else if body.contains(':') {
182 let parts: Vec<&str> = body.split(':').collect();
183 if parts.len() > 3 {
184 return Err(bad());
185 }
186 let mut total = 0.0;
187 for (i, part) in parts.iter().enumerate() {
188 let v = number(part)?;
189 if i + 1 < parts.len() && v.fract() != 0.0 {
190 return Err(bad());
191 }
192 total = total * 60.0 + v;
193 }
194 total
195 } else {
196 number(body)?
197 };
198 Ok(sign * seconds)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn fr(s: &str) -> FrameRate {
206 s.parse().unwrap()
207 }
208
209 #[test]
210 fn parses_frame_rates() {
211 assert_eq!(fr("30"), FrameRate::new(30, 1).unwrap());
212 assert_eq!(fr("29.97"), FrameRate::new(30000, 1001).unwrap());
213 assert_eq!(fr("23.976"), FrameRate::new(24000, 1001).unwrap());
214 assert_eq!(fr("23.98"), FrameRate::new(24000, 1001).unwrap());
215 assert_eq!(fr("59.94"), FrameRate::new(60000, 1001).unwrap());
216 assert_eq!(fr("119.88"), FrameRate::new(120000, 1001).unwrap());
217 assert_eq!(fr("60000/1001"), FrameRate::new(60000, 1001).unwrap());
218 assert_eq!(fr("50/2"), FrameRate::new(25, 1).unwrap());
219 assert_eq!(fr("12.5"), FrameRate::new(25, 2).unwrap());
220 assert!("0".parse::<FrameRate>().is_err());
221 assert!("abc".parse::<FrameRate>().is_err());
222 assert!("1000".parse::<FrameRate>().is_err());
223 }
224
225 #[test]
226 fn labels_and_display() {
227 assert_eq!(fr("29.97").to_string(), "30000/1001");
228 assert_eq!(fr("29.97").label(), "29.97");
229 assert_eq!(fr("23.976").label(), "23.976");
230 assert_eq!(fr("25").label(), "25");
231 assert_eq!(fr("29.97").nominal(), 30);
232 assert!(fr("59.94").is_drop_frame());
233 assert!(!fr("23.976").is_drop_frame());
234 }
235
236 #[test]
237 fn ntsc_sample_schedule_is_exact() {
238 let rate = fr("29.97");
239 assert_eq!(rate.frame_start_sample(30000, 48000), 1001 * 48000);
241 let lens: Vec<i64> = (0..10)
242 .map(|n| rate.frame_start_sample(n + 1, 48000) - rate.frame_start_sample(n, 48000))
243 .collect();
244 assert!(lens.iter().all(|&l| l == 1601 || l == 1602), "{lens:?}");
245 assert_eq!(fr("30").frame_start_sample(-1, 48000), -1600);
246 }
247
248 #[test]
249 fn parses_times() {
250 assert_eq!(parse_time("2.5").unwrap(), 2.5);
251 assert_eq!(parse_time("250ms").unwrap(), 0.25);
252 assert_eq!(parse_time("+78.5ms").unwrap(), 0.0785);
253 assert_eq!(parse_time("-40ms").unwrap(), -0.04);
254 assert_eq!(parse_time("1.5s").unwrap(), 1.5);
255 assert_eq!(parse_time("00:00:05").unwrap(), 5.0);
256 assert_eq!(parse_time("01:02.5").unwrap(), 62.5);
257 assert_eq!(parse_time("1:00:00").unwrap(), 3600.0);
258 assert!(parse_time("1.5:00").is_err());
259 assert!(parse_time("ms").is_err());
260 assert!(parse_time("nan").is_err());
261 }
262}