use crate::{FrameRateError, MediaTime, TICKS_PER_SECOND};
use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
#[serde(try_from = "Wire")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct FrameRate {
num: u32,
den: u32,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "schema",
derive(schemars::JsonSchema),
schemars(rename = "FrameRate")
)]
struct Wire {
#[cfg_attr(feature = "schema", schemars(range(min = 1)))]
num: u32,
#[cfg_attr(feature = "schema", schemars(range(min = 1)))]
den: u32,
}
impl TryFrom<Wire> for FrameRate {
type Error = FrameRateError;
fn try_from(wire: Wire) -> Result<Self, Self::Error> {
Self::new(wire.num, wire.den)
}
}
impl FrameRate {
pub const fn new(num: u32, den: u32) -> Result<Self, FrameRateError> {
if num == 0 {
return Err(FrameRateError::ZeroNumerator);
}
if den == 0 {
return Err(FrameRateError::ZeroDenominator);
}
if (num as u64) > (TICKS_PER_SECOND as u64) * (den as u64) {
return Err(FrameRateError::ExceedsTimelineResolution);
}
let divisor = gcd(num, den);
Ok(Self {
num: num / divisor,
den: den / divisor,
})
}
#[must_use]
pub const fn numerator(self) -> u32 {
self.num
}
#[must_use]
pub const fn denominator(self) -> u32 {
self.den
}
#[must_use]
pub fn frame_start(self, frame: i64) -> Option<MediaTime> {
let value = i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(self.den);
i64::try_from(value.div_euclid(i128::from(self.num)))
.ok()
.map(MediaTime)
}
#[must_use]
pub fn frame_at_or_before(self, time: MediaTime) -> Option<i64> {
let n = (i128::from(time.0) + 1) * i128::from(self.num);
let d = i128::from(TICKS_PER_SECOND) * i128::from(self.den);
i64::try_from(-(-n).div_euclid(d) - 1).ok()
}
}
impl fmt::Display for FrameRate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{} fps", self.num, self.den)
}
}
const fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_frame_lasts_at_least_one_tick() {
assert_eq!(
FrameRate::new(120_000, 1).map(FrameRate::numerator),
Ok(120_000)
);
assert_eq!(
FrameRate::new(240_000, 2).map(|rate| (rate.numerator(), rate.denominator())),
Ok((120_000, 1))
);
for (num, den) in [(120_001, 1), (240_001, 2), (u32::MAX, 35_791)] {
assert_eq!(
FrameRate::new(num, den),
Err(FrameRateError::ExceedsTimelineResolution),
"{num}/{den}"
);
}
assert_eq!(
FrameRate::new(u32::MAX, 35_792).map(FrameRate::numerator),
Ok(u32::MAX)
);
assert_eq!(FrameRate::new(0, 1), Err(FrameRateError::ZeroNumerator));
assert_eq!(FrameRate::new(1, 0), Err(FrameRateError::ZeroDenominator));
}
#[test]
fn a_rate_reduces() {
let rate = FrameRate::new(60, 2).unwrap();
assert_eq!((rate.numerator(), rate.denominator()), (30, 1));
assert_eq!(rate, FrameRate::new(30, 1).unwrap());
}
#[test]
fn the_wire_form_parses_only_what_new_accepts() {
let parse = |json: &str| serde_json::from_str::<FrameRate>(json);
assert_eq!(
parse(r#"{"num":60000,"den":2002}"#).unwrap(),
FrameRate::new(30_000, 1_001).unwrap()
);
assert_eq!(
parse(r#"{"num":120000,"den":1}"#).unwrap(),
FrameRate::new(120_000, 1).unwrap()
);
for (json, message) in [
(r#"{"num":120001,"den":1}"#, "exceeds timeline resolution"),
(r#"{"num":240001,"den":2}"#, "exceeds timeline resolution"),
(r#"{"num":0,"den":1}"#, "numerator must not be zero"),
(r#"{"num":30,"den":0}"#, "denominator must not be zero"),
(r#"{"num":4294967296,"den":1}"#, "expected u32"),
(r#"{"num":30,"den":4294967296}"#, "expected u32"),
(r#"{"num":-30,"den":1}"#, "expected u32"),
(r#"{"num":30.0,"den":1}"#, "expected u32"),
(r#"{"num":30,"den":1,"fps":30}"#, "unknown field `fps`"),
(r#"{"num":30}"#, "missing field `den`"),
] {
let error = parse(json).expect_err(json).to_string();
assert!(error.contains(message), "{json}: {error}");
}
}
const RATES: [(u32, u32); 11] = [
(30, 1),
(30_000, 1_001),
(90, 1),
(72, 1),
(144, 1),
(165, 1),
(48_000, 1_001),
(7, 1),
(2_997, 100),
(119_999, 1),
(120_000, 1),
];
#[test]
fn a_frame_starts_at_the_tick_its_exact_start_falls_in() {
let starts = |num, den| {
let rate = FrameRate::new(num, den).unwrap();
(-1..=3)
.map(|frame| rate.frame_start(frame).unwrap().ticks())
.collect::<Vec<_>>()
};
assert_eq!(starts(90, 1), [-1334, 0, 1333, 2666, 4000]);
assert_eq!(starts(48_000, 1_001), [-2503, 0, 2502, 5005, 7507]);
for (num, den) in RATES {
let rate = FrameRate::new(num, den).unwrap();
let exact =
|frame: i64| i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(den);
for frame in -1000..1000 {
let start = i128::from(rate.frame_start(frame).unwrap().ticks());
assert!(
start * i128::from(num) <= exact(frame)
&& exact(frame) < (start + 1) * i128::from(num),
"{rate}: frame {frame} starts at {start}"
);
}
}
}
#[test]
fn every_tick_of_a_frame_names_that_frame() {
for (num, den) in RATES {
let rate = FrameRate::new(num, den).unwrap();
for frame in -1000..1000 {
let start = rate.frame_start(frame).unwrap();
let next = rate.frame_start(frame + 1).unwrap();
assert!(start < next, "{rate}: frame {frame} lasts no tick");
for tick in [start.0, start.0 + (next.0 - start.0) / 2, next.0 - 1] {
assert_eq!(
rate.frame_at_or_before(MediaTime(tick)),
Some(frame),
"{rate}: tick {tick} of frame {frame} [{start}, {next})"
);
}
}
}
}
#[test]
fn a_frame_starts_only_where_its_start_is_a_tick() {
for (num, den) in RATES.into_iter().filter(|&rate| rate != (120_000, 1)) {
let rate = FrameRate::new(num, den).unwrap();
let last = rate.frame_at_or_before(MediaTime(i64::MAX)).unwrap();
let first = rate.frame_at_or_before(MediaTime(i64::MIN)).unwrap();
assert!(rate.frame_start(last).is_some(), "{rate}: frame {last}");
assert_eq!(rate.frame_start(last + 1), None, "{rate}: frame {last} + 1");
assert!(
rate.frame_start(first)
.is_none_or(|start| start == MediaTime(i64::MIN)),
"{rate}: frame {first}"
);
assert!(
rate.frame_start(first + 1).is_some(),
"{rate}: frame {first} + 1"
);
assert_eq!(rate.frame_start(i64::MAX), None, "{rate}");
assert_eq!(rate.frame_start(i64::MIN), None, "{rate}");
}
let fastest = FrameRate::new(120_000, 1).unwrap();
assert_eq!(
fastest.frame_at_or_before(MediaTime(i64::MAX)),
Some(i64::MAX)
);
assert_eq!(
fastest.frame_at_or_before(MediaTime(i64::MIN)),
Some(i64::MIN)
);
assert_eq!(fastest.frame_start(i64::MAX), Some(MediaTime(i64::MAX)));
assert_eq!(fastest.frame_start(i64::MIN), Some(MediaTime(i64::MIN)));
}
}