use std::{
sync::{
Arc, Weak,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
use ffmpeg_next as ffmpeg;
#[derive(Debug)]
pub struct FrameRate(AtomicU64);
impl FrameRate {
pub fn new(rate: ffmpeg::Rational) -> Arc<Self> {
Arc::new(Self(AtomicU64::new(pack(rate))))
}
pub fn get(&self) -> ffmpeg::Rational {
unpack(self.0.load(Ordering::Relaxed))
}
pub fn interval(&self) -> Duration {
let rate = self.get();
Duration::from_secs_f64(rate.denominator() as f64 / rate.numerator() as f64)
}
pub fn set(&self, rate: ffmpeg::Rational) -> bool {
if rate.numerator() <= 0 || rate.denominator() <= 0 {
return false;
}
self.0.store(pack(rate), Ordering::Relaxed);
true
}
pub fn handle(self: &Arc<Self>) -> FrameRateHandle {
FrameRateHandle(Arc::downgrade(self))
}
}
#[derive(Debug, Clone)]
pub struct FrameRateHandle(Weak<FrameRate>);
impl FrameRateHandle {
pub fn set(&self, rate: ffmpeg::Rational) -> bool {
self.0.upgrade().is_some_and(|shared| shared.set(rate))
}
pub fn get(&self) -> Option<ffmpeg::Rational> {
Some(self.0.upgrade()?.get())
}
}
fn pack(rate: ffmpeg::Rational) -> u64 {
((rate.numerator() as u32 as u64) << 32) | rate.denominator() as u32 as u64
}
fn unpack(packed: u64) -> ffmpeg::Rational {
ffmpeg::Rational::new((packed >> 32) as u32 as i32, packed as u32 as i32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_rate_survives_being_packed_and_read_back() {
for rate in [
ffmpeg::Rational::new(60, 1),
ffmpeg::Rational::new(30, 1),
ffmpeg::Rational::new(24000, 1001),
ffmpeg::Rational::new(1, 1),
] {
let shared = FrameRate::new(rate);
assert_eq!(shared.get(), rate, "{rate}");
assert_eq!(shared.handle().get(), Some(rate), "{rate}");
}
}
#[test]
fn the_interval_is_one_frame_long() {
let shared = FrameRate::new(ffmpeg::Rational::new(50, 1));
assert_eq!(shared.interval(), Duration::from_millis(20));
assert!(shared.handle().set(ffmpeg::Rational::new(25, 1)));
assert_eq!(shared.interval(), Duration::from_millis(40));
}
#[test]
fn an_impossible_rate_is_refused_and_changes_nothing() {
let shared = FrameRate::new(ffmpeg::Rational::new(60, 1));
let handle = shared.handle();
for refused in [
ffmpeg::Rational::new(0, 1),
ffmpeg::Rational::new(-30, 1),
ffmpeg::Rational::new(30, 0),
] {
assert!(!handle.set(refused), "{refused} was accepted");
assert_eq!(shared.get(), ffmpeg::Rational::new(60, 1));
}
}
#[test]
fn a_handle_outliving_its_element_answers_instead_of_taking_effect() {
let shared = FrameRate::new(ffmpeg::Rational::new(60, 1));
let handle = shared.handle();
drop(shared);
assert!(!handle.set(ffmpeg::Rational::new(30, 1)));
assert_eq!(handle.get(), None);
}
}