use std::time::Duration;
use crate::color::ColorSpace;
use crate::identify::{Container, Encoding};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Rational {
pub numerator: u32,
pub denominator: u32,
}
impl Rational {
pub fn new(numerator: u32, denominator: u32) -> Self {
Rational {
numerator,
denominator: denominator.max(1),
}
}
pub fn as_f64(self) -> f64 {
f64::from(self.numerator) / f64::from(self.denominator)
}
pub fn frame_duration(self) -> Duration {
if self.numerator == 0 {
return Duration::ZERO;
}
Duration::from_secs_f64(f64::from(self.denominator) / f64::from(self.numerator))
}
pub fn describe(self) -> String {
let rate = self.as_f64();
let name = match (self.numerator, self.denominator) {
(24000, 1001) => Some("23.976"),
(30000, 1001) => Some("29.97"),
(60000, 1001) => Some("59.94"),
_ => None,
};
match name {
Some(name) => format!("{name} fps"),
None => format!("{rate:.3} fps"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Rotation {
#[default]
None,
Quarter,
Half,
ThreeQuarters,
}
impl Rotation {
pub fn degrees(self) -> u32 {
match self {
Rotation::None => 0,
Rotation::Quarter => 90,
Rotation::Half => 180,
Rotation::ThreeQuarters => 270,
}
}
pub fn from_degrees(degrees: i32) -> Self {
match degrees.rem_euclid(360) {
0..=44 | 315..=359 => Rotation::None,
45..=134 => Rotation::Quarter,
135..=224 => Rotation::Half,
_ => Rotation::ThreeQuarters,
}
}
pub fn is_transposed(self) -> bool {
matches!(self, Rotation::Quarter | Rotation::ThreeQuarters)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TrackKind {
Video,
Audio,
Subtitle,
Other,
}
#[derive(Clone, Debug)]
pub struct TrackInfo {
pub id: u32,
pub kind: TrackKind,
pub encoding: Option<Encoding>,
pub codec_id: String,
pub width: u32,
pub height: u32,
pub frame_rate: Option<Rational>,
pub bit_depth: u32,
pub color: ColorSpace,
pub rotation: Rotation,
pub duration: Duration,
pub extra_data: Vec<u8>,
}
impl TrackInfo {
pub fn display_size(&self) -> (u32, u32) {
if self.rotation.is_transposed() {
(self.height, self.width)
} else {
(self.width, self.height)
}
}
pub fn is_decodable(&self) -> bool {
matches!(
self.encoding,
Some(
Encoding::H264
| Encoding::H265
| Encoding::Av1
| Encoding::Vp9
| Encoding::Vp8
| Encoding::Still
)
)
}
}
#[derive(Clone, Debug)]
pub struct MediaInfo {
pub container: Container,
pub tracks: Vec<TrackInfo>,
pub duration: Duration,
pub seekable: bool,
}
impl MediaInfo {
pub fn video(&self) -> Option<&TrackInfo> {
self.tracks
.iter()
.find(|track| track.kind == TrackKind::Video)
}
pub fn has_audio(&self) -> bool {
self.tracks
.iter()
.any(|track| track.kind == TrackKind::Audio)
}
pub fn is_royalty_free(&self) -> bool {
self.video()
.and_then(|track| track.encoding)
.is_some_and(Encoding::is_royalty_free)
}
}
#[derive(Clone, Debug)]
pub struct Packet {
pub track_id: u32,
pub data: Vec<u8>,
pub pts: Duration,
pub dts: Duration,
pub is_keyframe: bool,
}
impl Packet {
pub fn new(track_id: u32, data: Vec<u8>, pts: Duration, is_keyframe: bool) -> Self {
Packet {
track_id,
data,
pts,
dts: pts,
is_keyframe,
}
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn broadcast_rates_keep_their_thousand_over_thousand_and_one() {
let ntsc = Rational::new(30000, 1001);
assert!((ntsc.as_f64() - 29.97002997).abs() < 1e-6);
assert_eq!(ntsc.describe(), "29.97 fps");
assert!((ntsc.frame_duration().as_secs_f64() - 1001.0 / 30000.0).abs() < 1e-9);
}
#[test]
fn a_zero_denominator_does_not_divide_by_zero() {
let broken = Rational::new(24, 0);
assert_eq!(broken.denominator, 1);
assert!(broken.as_f64().is_finite());
}
#[test]
fn rotation_rounds_to_quarter_turns_and_transposes() {
assert_eq!(Rotation::from_degrees(90), Rotation::Quarter);
assert_eq!(Rotation::from_degrees(-90), Rotation::ThreeQuarters);
assert_eq!(Rotation::from_degrees(359), Rotation::None);
assert_eq!(Rotation::from_degrees(450), Rotation::Quarter);
assert!(Rotation::Quarter.is_transposed());
assert!(!Rotation::Half.is_transposed());
}
#[test]
fn a_rotated_track_reports_the_size_it_should_be_shown_at() {
let track = TrackInfo {
id: 1,
kind: TrackKind::Video,
encoding: Some(Encoding::H264),
codec_id: "avc1".to_string(),
width: 1920,
height: 1080,
frame_rate: Some(Rational::new(30, 1)),
bit_depth: 8,
color: ColorSpace::default(),
rotation: Rotation::Quarter,
duration: Duration::from_secs(10),
extra_data: Vec::new(),
};
assert_eq!(track.display_size(), (1080, 1920));
assert!(track.is_decodable());
}
#[test]
fn a_file_reports_its_video_track_and_whether_there_is_sound() {
let video = TrackInfo {
id: 1,
kind: TrackKind::Video,
encoding: Some(Encoding::Av1),
codec_id: "av01".to_string(),
width: 640,
height: 480,
frame_rate: None,
bit_depth: 8,
color: ColorSpace::default(),
rotation: Rotation::None,
duration: Duration::from_secs(1),
extra_data: Vec::new(),
};
let audio = TrackInfo {
id: 2,
kind: TrackKind::Audio,
encoding: None,
codec_id: "opus".to_string(),
..video.clone()
};
let info = MediaInfo {
container: Container::WebM,
tracks: vec![audio, video.clone()],
duration: Duration::from_secs(1),
seekable: true,
};
assert_eq!(info.video().unwrap().id, video.id);
assert!(info.has_audio(), "the audio track should be reported");
assert!(info.is_royalty_free());
}
}