Skip to main content

moq_video/
rate.rs

1//! Exact video frame rates.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::time::Duration;
6
7/// The largest supported video rate, in frames per second.
8pub const MAX_FRAMES_PER_SECOND: u32 = 1_000_000;
9
10/// An invalid video frame rate.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
12#[non_exhaustive]
13pub enum RateError {
14	/// The numerator or denominator was zero.
15	#[error("frame rate numerator and denominator must be non-zero")]
16	Zero,
17	/// The ratio exceeds the supported range.
18	#[error("frame rate must not exceed {MAX_FRAMES_PER_SECOND} frames per second")]
19	TooLarge,
20	/// A floating-point catalog rate was not finite and positive.
21	#[error("frame rate must be finite and positive")]
22	InvalidFloat,
23}
24
25/// An exact positive video frame rate, in frames per second.
26#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
27pub struct Rate {
28	numerator: u32,
29	denominator: u32,
30}
31
32impl Rate {
33	/// Construct an exact rate from a numerator and denominator.
34	pub fn new(numerator: u32, denominator: u32) -> Result<Self, RateError> {
35		if numerator == 0 || denominator == 0 {
36			return Err(RateError::Zero);
37		}
38		if u64::from(numerator) > u64::from(MAX_FRAMES_PER_SECOND) * u64::from(denominator) {
39			return Err(RateError::TooLarge);
40		}
41		let divisor = gcd(numerator, denominator);
42		Ok(Self {
43			numerator: numerator / divisor,
44			denominator: denominator / divisor,
45		})
46	}
47
48	/// Approximate a finite catalog rate using 32-bit rational components.
49	pub fn from_f64(value: f64) -> Result<Self, RateError> {
50		if !value.is_finite() || value <= 0.0 {
51			return Err(RateError::InvalidFloat);
52		}
53		if value > f64::from(MAX_FRAMES_PER_SECOND) {
54			return Err(RateError::TooLarge);
55		}
56		// Continued fractions recover conventional rates such as 30000/1001 from
57		// their JSON floating-point representation without inventing a decimal timebase.
58		let (mut input, mut n0, mut d0, mut n1, mut d1) = (value, 0u64, 1u64, 1u64, 0u64);
59		loop {
60			let whole = input.floor() as u64;
61			let Some(n2) = whole.checked_mul(n1).and_then(|v| v.checked_add(n0)) else {
62				break;
63			};
64			let Some(d2) = whole.checked_mul(d1).and_then(|v| v.checked_add(d0)) else {
65				break;
66			};
67			if n2 > u64::from(u32::MAX) || d2 > u64::from(u32::MAX) {
68				break;
69			}
70			(n0, d0, n1, d1) = (n1, d1, n2, d2);
71			let fraction = input - whole as f64;
72			if fraction < 1e-12 || (n1 as f64 / d1 as f64 - value).abs() < 1e-12 {
73				break;
74			}
75			input = 1.0 / fraction;
76		}
77		Self::new(n1 as u32, d1 as u32)
78	}
79
80	/// The frames-per-second numerator.
81	pub fn numerator(self) -> u32 {
82		self.numerator
83	}
84
85	/// The frames-per-second denominator.
86	pub fn denominator(self) -> u32 {
87		self.denominator
88	}
89
90	/// Convert the exact rate to a floating-point catalog value.
91	pub fn as_f64(self) -> f64 {
92		f64::from(self.numerator) / f64::from(self.denominator)
93	}
94
95	/// Round to the nearest whole frame rate for integer-only platform APIs.
96	pub fn rounded(self) -> u32 {
97		let numerator = u64::from(self.numerator);
98		let denominator = u64::from(self.denominator);
99		u32::try_from((numerator + denominator / 2) / denominator)
100			.unwrap_or(MAX_FRAMES_PER_SECOND)
101			.max(1)
102	}
103
104	/// Number of frames in `duration`, rounded to the nearest frame.
105	pub fn frames(self, duration: Duration) -> u32 {
106		let nanos = duration.as_nanos();
107		let scaled = nanos.saturating_mul(u128::from(self.numerator));
108		let divisor = 1_000_000_000u128 * u128::from(self.denominator);
109		u32::try_from((scaled + divisor / 2) / divisor).unwrap_or(u32::MAX)
110	}
111
112	#[cfg(feature = "capture")]
113	pub(crate) const fn integer(value: u32) -> Self {
114		Self {
115			numerator: value,
116			denominator: 1,
117		}
118	}
119}
120
121impl Ord for Rate {
122	fn cmp(&self, other: &Self) -> Ordering {
123		(u64::from(self.numerator) * u64::from(other.denominator))
124			.cmp(&(u64::from(other.numerator) * u64::from(self.denominator)))
125	}
126}
127
128impl PartialOrd for Rate {
129	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
130		Some(self.cmp(other))
131	}
132}
133
134impl fmt::Display for Rate {
135	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136		write!(f, "{}/{}", self.numerator, self.denominator)
137	}
138}
139
140const fn gcd(mut left: u32, mut right: u32) -> u32 {
141	while right != 0 {
142		let remainder = left % right;
143		left = right;
144		right = remainder;
145	}
146	left
147}
148
149#[cfg(test)]
150mod tests {
151	use super::*;
152
153	#[test]
154	fn preserves_broadcast_rates() {
155		assert_eq!(
156			Rate::new(30_000, 1_001).unwrap(),
157			Rate::from_f64(30_000.0 / 1_001.0).unwrap()
158		);
159		assert_eq!(
160			Rate::new(60_000, 1_001).unwrap(),
161			Rate::from_f64(60_000.0 / 1_001.0).unwrap()
162		);
163	}
164
165	#[test]
166	fn validates_components_and_range() {
167		assert_eq!(Rate::new(30, 0), Err(RateError::Zero));
168		assert_eq!(Rate::new(0, 1), Err(RateError::Zero));
169		assert_eq!(Rate::new(MAX_FRAMES_PER_SECOND + 1, 1), Err(RateError::TooLarge));
170	}
171
172	#[test]
173	fn rejects_invalid_floats() {
174		assert_eq!(Rate::from_f64(f64::NAN), Err(RateError::InvalidFloat));
175		assert_eq!(Rate::from_f64(f64::INFINITY), Err(RateError::InvalidFloat));
176		assert_eq!(Rate::from_f64(f64::NEG_INFINITY), Err(RateError::InvalidFloat));
177		assert_eq!(Rate::from_f64(0.0), Err(RateError::InvalidFloat));
178		assert_eq!(Rate::from_f64(-30.0), Err(RateError::InvalidFloat));
179		assert_eq!(
180			Rate::from_f64(f64::from(MAX_FRAMES_PER_SECOND) + 1.0),
181			Err(RateError::TooLarge)
182		);
183	}
184}