1use crate::types::{CodecId, PixelFormat, Rational, SampleFormat};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
48pub enum ColorSpace {
49 Bt601,
51 #[default]
53 Bt709,
54 Bt2020,
56 DisplayP3,
58 Rgb,
60 Unknown,
62}
63
64impl std::fmt::Display for ColorSpace {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 let s = match self {
67 Self::Bt601 => "bt601",
68 Self::Bt709 => "bt709",
69 Self::Bt2020 => "bt2020",
70 Self::DisplayP3 => "display_p3",
71 Self::Rgb => "rgb",
72 Self::Unknown => "unknown",
73 };
74 f.write_str(s)
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
84pub enum ChromaLocation {
85 #[default]
87 Left,
88 Centre,
90 Right,
92 Unspecified,
94}
95
96#[derive(Debug, Clone, PartialEq)]
102pub struct VideoParams {
103 pub width: u32,
105 pub height: u32,
107 pub frame_rate: Rational,
110 pub pixel_format: PixelFormat,
112 pub color_space: ColorSpace,
114 pub chroma_location: ChromaLocation,
116 pub display_aspect_ratio: Option<Rational>,
118 pub bit_depth: u8,
120 pub bitrate_bps: Option<u64>,
122}
123
124impl VideoParams {
125 #[must_use]
132 pub fn new(width: u32, height: u32, frame_rate: Rational) -> Self {
133 Self {
134 width,
135 height,
136 frame_rate,
137 pixel_format: PixelFormat::Yuv420p,
138 color_space: ColorSpace::Bt709,
139 chroma_location: ChromaLocation::Left,
140 display_aspect_ratio: None,
141 bit_depth: 8,
142 bitrate_bps: None,
143 }
144 }
145
146 #[must_use]
148 pub fn storage_aspect_ratio(&self) -> Rational {
149 Rational::new(self.width as i64, self.height as i64)
150 }
151
152 #[must_use]
155 pub fn effective_aspect_ratio(&self) -> Rational {
156 self.display_aspect_ratio
157 .unwrap_or_else(|| self.storage_aspect_ratio())
158 }
159
160 #[must_use]
162 pub fn is_valid(&self) -> bool {
163 self.width > 0 && self.height > 0 && self.frame_rate.den > 0
164 }
165
166 #[must_use]
168 pub fn pixel_count(&self) -> u64 {
169 u64::from(self.width) * u64::from(self.height)
170 }
171
172 #[must_use]
174 pub fn fps(&self) -> f64 {
175 self.frame_rate.to_f64()
176 }
177
178 #[must_use]
180 pub fn with_pixel_format(mut self, fmt: PixelFormat) -> Self {
181 self.pixel_format = fmt;
182 self
183 }
184
185 #[must_use]
187 pub fn with_color_space(mut self, cs: ColorSpace) -> Self {
188 self.color_space = cs;
189 self
190 }
191
192 #[must_use]
194 pub fn with_bit_depth(mut self, depth: u8) -> Self {
195 self.bit_depth = depth;
196 self
197 }
198
199 #[must_use]
201 pub fn with_bitrate(mut self, bps: u64) -> Self {
202 self.bitrate_bps = Some(bps);
203 self
204 }
205}
206
207#[derive(Debug, Clone, PartialEq)]
213pub struct AudioParams {
214 pub sample_rate: u32,
216 pub channels: u16,
218 pub sample_format: SampleFormat,
220 pub bitrate_bps: Option<u64>,
222 pub frame_size: Option<u32>,
226 pub loudness_lufs: Option<f32>,
228}
229
230impl AudioParams {
231 #[must_use]
235 pub fn new(sample_rate: u32, channels: u16) -> Self {
236 Self {
237 sample_rate,
238 channels,
239 sample_format: SampleFormat::F32,
240 bitrate_bps: None,
241 frame_size: None,
242 loudness_lufs: None,
243 }
244 }
245
246 #[must_use]
248 pub fn is_valid(&self) -> bool {
249 self.sample_rate > 0 && self.channels > 0
250 }
251
252 #[must_use]
255 pub fn frame_duration_secs(&self) -> Option<f64> {
256 self.frame_size
257 .map(|fs| f64::from(fs) / f64::from(self.sample_rate))
258 }
259
260 #[must_use]
262 pub fn with_sample_format(mut self, fmt: SampleFormat) -> Self {
263 self.sample_format = fmt;
264 self
265 }
266
267 #[must_use]
269 pub fn with_bitrate(mut self, bps: u64) -> Self {
270 self.bitrate_bps = Some(bps);
271 self
272 }
273
274 #[must_use]
276 pub fn with_frame_size(mut self, samples: u32) -> Self {
277 self.frame_size = Some(samples);
278 self
279 }
280
281 #[must_use]
283 pub fn with_loudness(mut self, lufs: f32) -> Self {
284 self.loudness_lufs = Some(lufs);
285 self
286 }
287}
288
289#[derive(Debug, Clone, PartialEq)]
295pub enum CodecParamsInner {
296 Video(VideoParams),
298 Audio(AudioParams),
300 Data,
302}
303
304#[derive(Debug, Clone, PartialEq)]
313pub struct CodecParams {
314 pub codec_id: CodecId,
316 pub inner: CodecParamsInner,
318 pub stream_index: Option<u32>,
320 pub language: Option<String>,
322}
323
324impl CodecParams {
325 #[must_use]
327 pub fn video(codec_id: CodecId, params: VideoParams) -> Self {
328 Self {
329 codec_id,
330 inner: CodecParamsInner::Video(params),
331 stream_index: None,
332 language: None,
333 }
334 }
335
336 #[must_use]
338 pub fn audio(codec_id: CodecId, params: AudioParams) -> Self {
339 Self {
340 codec_id,
341 inner: CodecParamsInner::Audio(params),
342 stream_index: None,
343 language: None,
344 }
345 }
346
347 #[must_use]
349 pub fn data(codec_id: CodecId) -> Self {
350 Self {
351 codec_id,
352 inner: CodecParamsInner::Data,
353 stream_index: None,
354 language: None,
355 }
356 }
357
358 #[must_use]
360 pub fn is_video(&self) -> bool {
361 matches!(self.inner, CodecParamsInner::Video(_))
362 }
363
364 #[must_use]
366 pub fn is_audio(&self) -> bool {
367 matches!(self.inner, CodecParamsInner::Audio(_))
368 }
369
370 #[must_use]
372 pub fn video_params(&self) -> Option<&VideoParams> {
373 if let CodecParamsInner::Video(ref v) = self.inner {
374 Some(v)
375 } else {
376 None
377 }
378 }
379
380 #[must_use]
382 pub fn audio_params(&self) -> Option<&AudioParams> {
383 if let CodecParamsInner::Audio(ref a) = self.inner {
384 Some(a)
385 } else {
386 None
387 }
388 }
389
390 #[must_use]
392 pub fn with_stream_index(mut self, index: u32) -> Self {
393 self.stream_index = Some(index);
394 self
395 }
396
397 #[must_use]
399 pub fn with_language(mut self, lang: impl Into<String>) -> Self {
400 self.language = Some(lang.into());
401 self
402 }
403}
404
405#[derive(Debug, Default, Clone)]
412pub struct CodecParamSet {
413 params: Vec<CodecParams>,
414}
415
416impl CodecParamSet {
417 #[must_use]
419 pub fn new() -> Self {
420 Self::default()
421 }
422
423 pub fn add(&mut self, p: CodecParams) {
425 self.params.push(p);
426 }
427
428 #[must_use]
430 pub fn len(&self) -> usize {
431 self.params.len()
432 }
433
434 #[must_use]
436 pub fn is_empty(&self) -> bool {
437 self.params.is_empty()
438 }
439
440 #[must_use]
442 pub fn get(&self, index: usize) -> Option<&CodecParams> {
443 self.params.get(index)
444 }
445
446 pub fn iter(&self) -> impl Iterator<Item = &CodecParams> {
448 self.params.iter()
449 }
450
451 pub fn video_streams(&self) -> impl Iterator<Item = &CodecParams> {
453 self.params.iter().filter(|p| p.is_video())
454 }
455
456 pub fn audio_streams(&self) -> impl Iterator<Item = &CodecParams> {
458 self.params.iter().filter(|p| p.is_audio())
459 }
460
461 #[must_use]
463 pub fn first_video(&self) -> Option<&CodecParams> {
464 self.params.iter().find(|p| p.is_video())
465 }
466
467 #[must_use]
469 pub fn first_audio(&self) -> Option<&CodecParams> {
470 self.params.iter().find(|p| p.is_audio())
471 }
472}
473
474#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::types::{CodecId, PixelFormat, Rational, SampleFormat};
482
483 #[test]
486 fn test_color_space_display() {
487 assert_eq!(format!("{}", ColorSpace::Bt709), "bt709");
488 assert_eq!(format!("{}", ColorSpace::Bt2020), "bt2020");
489 assert_eq!(format!("{}", ColorSpace::Rgb), "rgb");
490 }
491
492 #[test]
493 fn test_color_space_default() {
494 let cs = ColorSpace::default();
495 assert_eq!(cs, ColorSpace::Bt709);
496 }
497
498 #[test]
501 fn test_video_params_basic() {
502 let vp = VideoParams::new(1920, 1080, Rational::new(30, 1));
503 assert_eq!(vp.width, 1920);
504 assert_eq!(vp.height, 1080);
505 assert!(vp.is_valid());
506 assert_eq!(vp.pixel_count(), 1920 * 1080);
507 }
508
509 #[test]
510 fn test_video_params_fps() {
511 let vp = VideoParams::new(1280, 720, Rational::new(30000, 1001));
512 let fps = vp.fps();
513 assert!((fps - 29.97).abs() < 0.01, "fps={fps}");
514 }
515
516 #[test]
517 fn test_video_params_aspect_ratio_fallback() {
518 let vp = VideoParams::new(1920, 1080, Rational::new(30, 1));
519 let sar = vp.storage_aspect_ratio();
520 let ear = vp.effective_aspect_ratio();
521 assert_eq!(sar, ear);
522 }
523
524 #[test]
525 fn test_video_params_display_aspect_override() {
526 let mut vp = VideoParams::new(720, 576, Rational::new(25, 1));
527 vp.display_aspect_ratio = Some(Rational::new(16, 9));
528 let ear = vp.effective_aspect_ratio();
529 assert_eq!(ear, Rational::new(16, 9));
530 }
531
532 #[test]
533 fn test_video_params_builder_chain() {
534 let vp = VideoParams::new(3840, 2160, Rational::new(60, 1))
535 .with_pixel_format(PixelFormat::Yuv420p)
536 .with_color_space(ColorSpace::Bt2020)
537 .with_bit_depth(10)
538 .with_bitrate(20_000_000);
539 assert_eq!(vp.bit_depth, 10);
540 assert_eq!(vp.color_space, ColorSpace::Bt2020);
541 assert_eq!(vp.bitrate_bps, Some(20_000_000));
542 }
543
544 #[test]
547 fn test_audio_params_basic() {
548 let ap = AudioParams::new(48_000, 2);
549 assert_eq!(ap.sample_rate, 48_000);
550 assert_eq!(ap.channels, 2);
551 assert!(ap.is_valid());
552 }
553
554 #[test]
555 fn test_audio_params_frame_duration() {
556 let ap = AudioParams::new(48_000, 2).with_frame_size(960);
557 let dur = ap.frame_duration_secs().expect("frame size set");
558 assert!((dur - 0.02).abs() < 1e-9, "expected 20ms, got {dur}");
559 }
560
561 #[test]
562 fn test_audio_params_builder_chain() {
563 let ap = AudioParams::new(44_100, 1)
564 .with_sample_format(SampleFormat::S16)
565 .with_bitrate(128_000)
566 .with_loudness(-23.0);
567 assert_eq!(ap.sample_format, SampleFormat::S16);
568 assert_eq!(ap.bitrate_bps, Some(128_000));
569 assert!((ap.loudness_lufs.unwrap_or(0.0) - (-23.0_f32)).abs() < 1e-5);
570 }
571
572 #[test]
575 fn test_codec_params_video() {
576 let cp = CodecParams::video(
577 CodecId::Av1,
578 VideoParams::new(1920, 1080, Rational::new(30, 1)),
579 );
580 assert!(cp.is_video());
581 assert!(!cp.is_audio());
582 assert_eq!(cp.video_params().map(|v| v.width), Some(1920));
583 assert!(cp.audio_params().is_none());
584 }
585
586 #[test]
587 fn test_codec_params_audio() {
588 let cp = CodecParams::audio(CodecId::Opus, AudioParams::new(48_000, 2));
589 assert!(cp.is_audio());
590 assert!(!cp.is_video());
591 assert_eq!(cp.audio_params().map(|a| a.channels), Some(2));
592 assert!(cp.video_params().is_none());
593 }
594
595 #[test]
596 fn test_codec_params_data() {
597 let cp = CodecParams::data(CodecId::WebVtt);
598 assert!(!cp.is_video());
599 assert!(!cp.is_audio());
600 }
601
602 #[test]
603 fn test_codec_params_language_and_stream_index() {
604 let cp = CodecParams::audio(CodecId::Vorbis, AudioParams::new(44_100, 2))
605 .with_stream_index(1)
606 .with_language("ja");
607 assert_eq!(cp.stream_index, Some(1));
608 assert_eq!(cp.language.as_deref(), Some("ja"));
609 }
610
611 #[test]
614 fn test_codec_param_set_push_and_query() {
615 let mut set = CodecParamSet::new();
616 assert!(set.is_empty());
617
618 set.add(CodecParams::video(
619 CodecId::Vp9,
620 VideoParams::new(1280, 720, Rational::new(24, 1)),
621 ));
622 set.add(CodecParams::audio(
623 CodecId::Opus,
624 AudioParams::new(48_000, 2),
625 ));
626 set.add(CodecParams::audio(
627 CodecId::Flac,
628 AudioParams::new(96_000, 2),
629 ));
630
631 assert_eq!(set.len(), 3);
632 assert_eq!(set.video_streams().count(), 1);
633 assert_eq!(set.audio_streams().count(), 2);
634 assert!(set.first_video().is_some());
635 assert!(set.first_audio().is_some());
636 }
637}