use crate::types::{CodecId, PixelFormat, Rational, SampleFormat};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ColorSpace {
Bt601,
#[default]
Bt709,
Bt2020,
DisplayP3,
Rgb,
Unknown,
}
impl std::fmt::Display for ColorSpace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Bt601 => "bt601",
Self::Bt709 => "bt709",
Self::Bt2020 => "bt2020",
Self::DisplayP3 => "display_p3",
Self::Rgb => "rgb",
Self::Unknown => "unknown",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ChromaLocation {
#[default]
Left,
Centre,
Right,
Unspecified,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VideoParams {
pub width: u32,
pub height: u32,
pub frame_rate: Rational,
pub pixel_format: PixelFormat,
pub color_space: ColorSpace,
pub chroma_location: ChromaLocation,
pub display_aspect_ratio: Option<Rational>,
pub bit_depth: u8,
pub bitrate_bps: Option<u64>,
}
impl VideoParams {
#[must_use]
pub fn new(width: u32, height: u32, frame_rate: Rational) -> Self {
Self {
width,
height,
frame_rate,
pixel_format: PixelFormat::Yuv420p,
color_space: ColorSpace::Bt709,
chroma_location: ChromaLocation::Left,
display_aspect_ratio: None,
bit_depth: 8,
bitrate_bps: None,
}
}
#[must_use]
pub fn storage_aspect_ratio(&self) -> Rational {
Rational::new(self.width as i64, self.height as i64)
}
#[must_use]
pub fn effective_aspect_ratio(&self) -> Rational {
self.display_aspect_ratio
.unwrap_or_else(|| self.storage_aspect_ratio())
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.width > 0 && self.height > 0 && self.frame_rate.den > 0
}
#[must_use]
pub fn pixel_count(&self) -> u64 {
u64::from(self.width) * u64::from(self.height)
}
#[must_use]
pub fn fps(&self) -> f64 {
self.frame_rate.to_f64()
}
#[must_use]
pub fn with_pixel_format(mut self, fmt: PixelFormat) -> Self {
self.pixel_format = fmt;
self
}
#[must_use]
pub fn with_color_space(mut self, cs: ColorSpace) -> Self {
self.color_space = cs;
self
}
#[must_use]
pub fn with_bit_depth(mut self, depth: u8) -> Self {
self.bit_depth = depth;
self
}
#[must_use]
pub fn with_bitrate(mut self, bps: u64) -> Self {
self.bitrate_bps = Some(bps);
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AudioParams {
pub sample_rate: u32,
pub channels: u16,
pub sample_format: SampleFormat,
pub bitrate_bps: Option<u64>,
pub frame_size: Option<u32>,
pub loudness_lufs: Option<f32>,
}
impl AudioParams {
#[must_use]
pub fn new(sample_rate: u32, channels: u16) -> Self {
Self {
sample_rate,
channels,
sample_format: SampleFormat::F32,
bitrate_bps: None,
frame_size: None,
loudness_lufs: None,
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.sample_rate > 0 && self.channels > 0
}
#[must_use]
pub fn frame_duration_secs(&self) -> Option<f64> {
self.frame_size
.map(|fs| f64::from(fs) / f64::from(self.sample_rate))
}
#[must_use]
pub fn with_sample_format(mut self, fmt: SampleFormat) -> Self {
self.sample_format = fmt;
self
}
#[must_use]
pub fn with_bitrate(mut self, bps: u64) -> Self {
self.bitrate_bps = Some(bps);
self
}
#[must_use]
pub fn with_frame_size(mut self, samples: u32) -> Self {
self.frame_size = Some(samples);
self
}
#[must_use]
pub fn with_loudness(mut self, lufs: f32) -> Self {
self.loudness_lufs = Some(lufs);
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CodecParamsInner {
Video(VideoParams),
Audio(AudioParams),
Data,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CodecParams {
pub codec_id: CodecId,
pub inner: CodecParamsInner,
pub stream_index: Option<u32>,
pub language: Option<String>,
}
impl CodecParams {
#[must_use]
pub fn video(codec_id: CodecId, params: VideoParams) -> Self {
Self {
codec_id,
inner: CodecParamsInner::Video(params),
stream_index: None,
language: None,
}
}
#[must_use]
pub fn audio(codec_id: CodecId, params: AudioParams) -> Self {
Self {
codec_id,
inner: CodecParamsInner::Audio(params),
stream_index: None,
language: None,
}
}
#[must_use]
pub fn data(codec_id: CodecId) -> Self {
Self {
codec_id,
inner: CodecParamsInner::Data,
stream_index: None,
language: None,
}
}
#[must_use]
pub fn is_video(&self) -> bool {
matches!(self.inner, CodecParamsInner::Video(_))
}
#[must_use]
pub fn is_audio(&self) -> bool {
matches!(self.inner, CodecParamsInner::Audio(_))
}
#[must_use]
pub fn video_params(&self) -> Option<&VideoParams> {
if let CodecParamsInner::Video(ref v) = self.inner {
Some(v)
} else {
None
}
}
#[must_use]
pub fn audio_params(&self) -> Option<&AudioParams> {
if let CodecParamsInner::Audio(ref a) = self.inner {
Some(a)
} else {
None
}
}
#[must_use]
pub fn with_stream_index(mut self, index: u32) -> Self {
self.stream_index = Some(index);
self
}
#[must_use]
pub fn with_language(mut self, lang: impl Into<String>) -> Self {
self.language = Some(lang.into());
self
}
}
#[derive(Debug, Default, Clone)]
pub struct CodecParamSet {
params: Vec<CodecParams>,
}
impl CodecParamSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, p: CodecParams) {
self.params.push(p);
}
#[must_use]
pub fn len(&self) -> usize {
self.params.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.params.is_empty()
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&CodecParams> {
self.params.get(index)
}
pub fn iter(&self) -> impl Iterator<Item = &CodecParams> {
self.params.iter()
}
pub fn video_streams(&self) -> impl Iterator<Item = &CodecParams> {
self.params.iter().filter(|p| p.is_video())
}
pub fn audio_streams(&self) -> impl Iterator<Item = &CodecParams> {
self.params.iter().filter(|p| p.is_audio())
}
#[must_use]
pub fn first_video(&self) -> Option<&CodecParams> {
self.params.iter().find(|p| p.is_video())
}
#[must_use]
pub fn first_audio(&self) -> Option<&CodecParams> {
self.params.iter().find(|p| p.is_audio())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CodecId, PixelFormat, Rational, SampleFormat};
#[test]
fn test_color_space_display() {
assert_eq!(format!("{}", ColorSpace::Bt709), "bt709");
assert_eq!(format!("{}", ColorSpace::Bt2020), "bt2020");
assert_eq!(format!("{}", ColorSpace::Rgb), "rgb");
}
#[test]
fn test_color_space_default() {
let cs = ColorSpace::default();
assert_eq!(cs, ColorSpace::Bt709);
}
#[test]
fn test_video_params_basic() {
let vp = VideoParams::new(1920, 1080, Rational::new(30, 1));
assert_eq!(vp.width, 1920);
assert_eq!(vp.height, 1080);
assert!(vp.is_valid());
assert_eq!(vp.pixel_count(), 1920 * 1080);
}
#[test]
fn test_video_params_fps() {
let vp = VideoParams::new(1280, 720, Rational::new(30000, 1001));
let fps = vp.fps();
assert!((fps - 29.97).abs() < 0.01, "fps={fps}");
}
#[test]
fn test_video_params_aspect_ratio_fallback() {
let vp = VideoParams::new(1920, 1080, Rational::new(30, 1));
let sar = vp.storage_aspect_ratio();
let ear = vp.effective_aspect_ratio();
assert_eq!(sar, ear);
}
#[test]
fn test_video_params_display_aspect_override() {
let mut vp = VideoParams::new(720, 576, Rational::new(25, 1));
vp.display_aspect_ratio = Some(Rational::new(16, 9));
let ear = vp.effective_aspect_ratio();
assert_eq!(ear, Rational::new(16, 9));
}
#[test]
fn test_video_params_builder_chain() {
let vp = VideoParams::new(3840, 2160, Rational::new(60, 1))
.with_pixel_format(PixelFormat::Yuv420p)
.with_color_space(ColorSpace::Bt2020)
.with_bit_depth(10)
.with_bitrate(20_000_000);
assert_eq!(vp.bit_depth, 10);
assert_eq!(vp.color_space, ColorSpace::Bt2020);
assert_eq!(vp.bitrate_bps, Some(20_000_000));
}
#[test]
fn test_audio_params_basic() {
let ap = AudioParams::new(48_000, 2);
assert_eq!(ap.sample_rate, 48_000);
assert_eq!(ap.channels, 2);
assert!(ap.is_valid());
}
#[test]
fn test_audio_params_frame_duration() {
let ap = AudioParams::new(48_000, 2).with_frame_size(960);
let dur = ap.frame_duration_secs().expect("frame size set");
assert!((dur - 0.02).abs() < 1e-9, "expected 20ms, got {dur}");
}
#[test]
fn test_audio_params_builder_chain() {
let ap = AudioParams::new(44_100, 1)
.with_sample_format(SampleFormat::S16)
.with_bitrate(128_000)
.with_loudness(-23.0);
assert_eq!(ap.sample_format, SampleFormat::S16);
assert_eq!(ap.bitrate_bps, Some(128_000));
assert!((ap.loudness_lufs.unwrap_or(0.0) - (-23.0_f32)).abs() < 1e-5);
}
#[test]
fn test_codec_params_video() {
let cp = CodecParams::video(
CodecId::Av1,
VideoParams::new(1920, 1080, Rational::new(30, 1)),
);
assert!(cp.is_video());
assert!(!cp.is_audio());
assert_eq!(cp.video_params().map(|v| v.width), Some(1920));
assert!(cp.audio_params().is_none());
}
#[test]
fn test_codec_params_audio() {
let cp = CodecParams::audio(CodecId::Opus, AudioParams::new(48_000, 2));
assert!(cp.is_audio());
assert!(!cp.is_video());
assert_eq!(cp.audio_params().map(|a| a.channels), Some(2));
assert!(cp.video_params().is_none());
}
#[test]
fn test_codec_params_data() {
let cp = CodecParams::data(CodecId::WebVtt);
assert!(!cp.is_video());
assert!(!cp.is_audio());
}
#[test]
fn test_codec_params_language_and_stream_index() {
let cp = CodecParams::audio(CodecId::Vorbis, AudioParams::new(44_100, 2))
.with_stream_index(1)
.with_language("ja");
assert_eq!(cp.stream_index, Some(1));
assert_eq!(cp.language.as_deref(), Some("ja"));
}
#[test]
fn test_codec_param_set_push_and_query() {
let mut set = CodecParamSet::new();
assert!(set.is_empty());
set.add(CodecParams::video(
CodecId::Vp9,
VideoParams::new(1280, 720, Rational::new(24, 1)),
));
set.add(CodecParams::audio(
CodecId::Opus,
AudioParams::new(48_000, 2),
));
set.add(CodecParams::audio(
CodecId::Flac,
AudioParams::new(96_000, 2),
));
assert_eq!(set.len(), 3);
assert_eq!(set.video_streams().count(), 1);
assert_eq!(set.audio_streams().count(), 2);
assert!(set.first_video().is_some());
assert!(set.first_audio().is_some());
}
}