use ffmpeg_next as ffmpeg;
use ffmpeg_next::Rescale;
use thiserror::Error as ThisError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ThisError)]
#[error(
"invalid time base {numerator}/{denominator}: both numerator and denominator must be positive"
)]
pub struct InvalidTimeBase {
pub numerator: i32,
pub denominator: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeBase(ffmpeg::Rational);
impl TimeBase {
pub fn try_new(value: ffmpeg::Rational) -> Result<Self, InvalidTimeBase> {
if value.numerator() <= 0 || value.denominator() <= 0 {
return Err(InvalidTimeBase {
numerator: value.numerator(),
denominator: value.denominator(),
});
}
Ok(Self(value))
}
pub(crate) fn new_unchecked(value: ffmpeg::Rational) -> Self {
Self(value)
}
#[cfg(feature = "webrtc")]
pub fn get(self) -> ffmpeg::Rational {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct MediaTimestamp {
pts: i64,
time_base: TimeBase,
}
impl MediaTimestamp {
#[cfg(feature = "webrtc")]
pub fn try_new(pts: i64, time_base: ffmpeg::Rational) -> Result<Self, InvalidTimeBase> {
Ok(Self {
pts,
time_base: TimeBase::try_new(time_base)?,
})
}
pub(crate) fn new_unchecked(pts: i64, time_base: TimeBase) -> Self {
Self { pts, time_base }
}
#[cfg(feature = "webrtc")]
pub fn pts(self) -> i64 {
self.pts
}
#[cfg(feature = "webrtc")]
pub fn time_base(self) -> TimeBase {
self.time_base
}
pub fn rescale(self, target: TimeBase) -> i64 {
self.pts.rescale(self.time_base.0, target.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_new_rejects_non_positive_numerator_or_denominator() {
for (numerator, denominator) in [(0, 1), (1, 0), (-1, 1), (1, -1), (0, 0)] {
let rational = ffmpeg::Rational::new(numerator, denominator);
assert_eq!(
TimeBase::try_new(rational),
Err(InvalidTimeBase {
numerator,
denominator
}),
"expected {numerator}/{denominator} to be rejected"
);
}
}
#[cfg(feature = "webrtc")]
#[test]
fn media_timestamp_try_new_rejects_the_same_invalid_time_bases() {
for (numerator, denominator) in [(0, 1), (1, 0), (-1, 1), (1, -1), (0, 0)] {
let rational = ffmpeg::Rational::new(numerator, denominator);
assert!(MediaTimestamp::try_new(1, rational).is_err());
}
}
#[test]
fn rescale_matches_hand_computed_seconds() {
let time_base = TimeBase::try_new(ffmpeg::Rational::new(1001, 30_000)).unwrap();
let timestamp = MediaTimestamp::new_unchecked(30, time_base);
let seconds = TimeBase::try_new(ffmpeg::Rational::new(1, 30_000)).unwrap();
assert_eq!(timestamp.rescale(seconds), 30_030);
}
#[test]
fn rescale_is_exact_for_a_unit_numerator_time_base() {
let time_base = TimeBase::try_new(ffmpeg::Rational::new(1, 90_000)).unwrap();
let timestamp = MediaTimestamp::new_unchecked(3_000, time_base);
let same_base = TimeBase::try_new(ffmpeg::Rational::new(1, 90_000)).unwrap();
assert_eq!(timestamp.rescale(same_base), 3_000);
}
}