Skip to main content

oximedia_core/
codec_params.rs

1//! Codec parameter types for video and audio streams.
2//!
3//! Provides codec-agnostic wrappers around common encoding/decoding parameters
4//! such as resolution, frame rate, sample rate, channel count, and bit depth.
5//! These types decouple codec configuration from specific codec implementations
6//! and allow parameter passing through the pipeline without importing
7//! heavy codec crates.
8//!
9//! # Structure
10//!
11//! - [`VideoParams`] — width, height, frame rate, pixel format, colour space
12//! - [`AudioParams`] — sample rate, channel count, sample format, bit rate
13//! - [`CodecParams`] — codec-agnostic union of video or audio params with a
14//!   [`CodecId`] label
15//! - `CodecParamsBuilder` — ergonomic builder for [`CodecParams`]
16//!
17//! # Example
18//!
19//! ```
20//! use oximedia_core::codec_params::{AudioParams, CodecParams, VideoParams};
21//! use oximedia_core::types::{CodecId, PixelFormat, Rational, SampleFormat};
22//!
23//! let video = CodecParams::video(
24//!     CodecId::Av1,
25//!     VideoParams::new(1920, 1080, Rational::new(30, 1)),
26//! );
27//!
28//! assert!(video.is_video());
29//! assert_eq!(video.video_params().map(|v| v.width), Some(1920));
30//!
31//! let audio = CodecParams::audio(
32//!     CodecId::Opus,
33//!     AudioParams::new(48_000, 2),
34//! );
35//!
36//! assert!(audio.is_audio());
37//! assert_eq!(audio.audio_params().map(|a| a.sample_rate), Some(48_000));
38//! ```
39
40use crate::types::{CodecId, PixelFormat, Rational, SampleFormat};
41
42// ---------------------------------------------------------------------------
43// ColorSpace
44// ---------------------------------------------------------------------------
45
46/// Colour-space / matrix coefficients used for YCbCr ↔ RGB conversion.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
48pub enum ColorSpace {
49    /// ITU-R BT.601 (standard definition).
50    Bt601,
51    /// ITU-R BT.709 (high definition, most common).
52    #[default]
53    Bt709,
54    /// ITU-R BT.2020 (ultra-high definition, HDR).
55    Bt2020,
56    /// Display P3 (wide colour gamut, common on Apple devices).
57    DisplayP3,
58    /// Identity / RGB pass-through (no conversion required).
59    Rgb,
60    /// Unknown / unspecified.
61    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// ---------------------------------------------------------------------------
79// ChromaLocation
80// ---------------------------------------------------------------------------
81
82/// Chroma sample location for sub-sampled formats (e.g. YUV 4:2:0).
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
84pub enum ChromaLocation {
85    /// Chroma samples are co-sited with the top-left luma sample.
86    #[default]
87    Left,
88    /// Chroma samples are centred vertically between luma rows (MPEG-1 style).
89    Centre,
90    /// Chroma samples are co-sited with the top-right luma sample.
91    Right,
92    /// Unspecified / unknown location.
93    Unspecified,
94}
95
96// ---------------------------------------------------------------------------
97// VideoParams
98// ---------------------------------------------------------------------------
99
100/// Video stream encoding / decoding parameters.
101#[derive(Debug, Clone, PartialEq)]
102pub struct VideoParams {
103    /// Frame width in pixels.
104    pub width: u32,
105    /// Frame height in pixels.
106    pub height: u32,
107    /// Frame rate as a rational number (e.g. `Rational::new(30, 1)` for 30 fps,
108    /// `Rational::new(30000, 1001)` for ≈29.97 fps).
109    pub frame_rate: Rational,
110    /// Pixel format (default: [`PixelFormat::Yuv420p`]).
111    pub pixel_format: PixelFormat,
112    /// Colour space (default: [`ColorSpace::Bt709`]).
113    pub color_space: ColorSpace,
114    /// Chroma sample location (default: [`ChromaLocation::Left`]).
115    pub chroma_location: ChromaLocation,
116    /// Display aspect ratio, if different from the storage aspect ratio.
117    pub display_aspect_ratio: Option<Rational>,
118    /// Bit depth per colour component (8 for standard, 10 or 12 for HDR).
119    pub bit_depth: u8,
120    /// Target or measured peak bitrate in bits per second, if known.
121    pub bitrate_bps: Option<u64>,
122}
123
124impl VideoParams {
125    /// Creates minimal `VideoParams` with `width`, `height`, and `frame_rate`.
126    ///
127    /// All other fields receive sensible defaults:
128    /// - `pixel_format` = [`PixelFormat::Yuv420p`]
129    /// - `color_space` = [`ColorSpace::Bt709`]
130    /// - `bit_depth` = 8
131    #[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    /// Returns the storage aspect ratio `width / height` as a `Rational`.
147    #[must_use]
148    pub fn storage_aspect_ratio(&self) -> Rational {
149        Rational::new(self.width as i64, self.height as i64)
150    }
151
152    /// Returns the display aspect ratio, falling back to the storage aspect
153    /// ratio when not explicitly set.
154    #[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    /// Returns `true` if the frame dimensions are valid (both non-zero).
161    #[must_use]
162    pub fn is_valid(&self) -> bool {
163        self.width > 0 && self.height > 0 && self.frame_rate.den > 0
164    }
165
166    /// Returns the total number of pixels per frame.
167    #[must_use]
168    pub fn pixel_count(&self) -> u64 {
169        u64::from(self.width) * u64::from(self.height)
170    }
171
172    /// Returns the frame rate as a floating-point number.
173    #[must_use]
174    pub fn fps(&self) -> f64 {
175        self.frame_rate.to_f64()
176    }
177
178    /// Builder-style setter for pixel format.
179    #[must_use]
180    pub fn with_pixel_format(mut self, fmt: PixelFormat) -> Self {
181        self.pixel_format = fmt;
182        self
183    }
184
185    /// Builder-style setter for colour space.
186    #[must_use]
187    pub fn with_color_space(mut self, cs: ColorSpace) -> Self {
188        self.color_space = cs;
189        self
190    }
191
192    /// Builder-style setter for bit depth.
193    #[must_use]
194    pub fn with_bit_depth(mut self, depth: u8) -> Self {
195        self.bit_depth = depth;
196        self
197    }
198
199    /// Builder-style setter for bitrate.
200    #[must_use]
201    pub fn with_bitrate(mut self, bps: u64) -> Self {
202        self.bitrate_bps = Some(bps);
203        self
204    }
205}
206
207// ---------------------------------------------------------------------------
208// AudioParams
209// ---------------------------------------------------------------------------
210
211/// Audio stream encoding / decoding parameters.
212#[derive(Debug, Clone, PartialEq)]
213pub struct AudioParams {
214    /// Sample rate in Hz (e.g. 48_000 for broadcast audio).
215    pub sample_rate: u32,
216    /// Number of audio channels (1 = mono, 2 = stereo, 6 = 5.1, …).
217    pub channels: u16,
218    /// Sample format (default: [`SampleFormat::F32`]).
219    pub sample_format: SampleFormat,
220    /// Target or measured peak bitrate in bits per second, if known.
221    pub bitrate_bps: Option<u64>,
222    /// Frame size (samples per channel per frame), if fixed by the codec.
223    ///
224    /// For example, Opus uses 20 ms frames at 48 kHz → 960 samples.
225    pub frame_size: Option<u32>,
226    /// Normalisation loudness target in LUFS, if known (e.g. −23 LUFS for EBU R128).
227    pub loudness_lufs: Option<f32>,
228}
229
230impl AudioParams {
231    /// Creates minimal `AudioParams` with `sample_rate` and `channels`.
232    ///
233    /// Defaults: `sample_format` = [`SampleFormat::F32`], other fields `None`.
234    #[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    /// Returns `true` if the parameters are logically valid.
247    #[must_use]
248    pub fn is_valid(&self) -> bool {
249        self.sample_rate > 0 && self.channels > 0
250    }
251
252    /// Returns the duration of a single frame in seconds, or `None` if no
253    /// fixed frame size is set.
254    #[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    /// Builder-style setter for sample format.
261    #[must_use]
262    pub fn with_sample_format(mut self, fmt: SampleFormat) -> Self {
263        self.sample_format = fmt;
264        self
265    }
266
267    /// Builder-style setter for bitrate.
268    #[must_use]
269    pub fn with_bitrate(mut self, bps: u64) -> Self {
270        self.bitrate_bps = Some(bps);
271        self
272    }
273
274    /// Builder-style setter for frame size.
275    #[must_use]
276    pub fn with_frame_size(mut self, samples: u32) -> Self {
277        self.frame_size = Some(samples);
278        self
279    }
280
281    /// Builder-style setter for loudness target.
282    #[must_use]
283    pub fn with_loudness(mut self, lufs: f32) -> Self {
284        self.loudness_lufs = Some(lufs);
285        self
286    }
287}
288
289// ---------------------------------------------------------------------------
290// CodecParamsInner
291// ---------------------------------------------------------------------------
292
293/// Inner payload of a [`CodecParams`] discriminated by media type.
294#[derive(Debug, Clone, PartialEq)]
295pub enum CodecParamsInner {
296    /// Video codec parameters.
297    Video(VideoParams),
298    /// Audio codec parameters.
299    Audio(AudioParams),
300    /// Data / subtitle / muxed stream with no further type-specific fields.
301    Data,
302}
303
304// ---------------------------------------------------------------------------
305// CodecParams
306// ---------------------------------------------------------------------------
307
308/// Codec-agnostic parameter descriptor for a single elementary stream.
309///
310/// Combines a [`CodecId`] with either [`VideoParams`], [`AudioParams`], or a
311/// bare `Data` marker for subtitle/attachment streams.
312#[derive(Debug, Clone, PartialEq)]
313pub struct CodecParams {
314    /// The codec used to encode this stream.
315    pub codec_id: CodecId,
316    /// Type-specific parameters.
317    pub inner: CodecParamsInner,
318    /// Optional stream index within the container (0-based).
319    pub stream_index: Option<u32>,
320    /// Optional stream language tag (BCP-47, e.g. `"en"`, `"ja"`).
321    pub language: Option<String>,
322}
323
324impl CodecParams {
325    /// Creates a `CodecParams` for a video stream.
326    #[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    /// Creates a `CodecParams` for an audio stream.
337    #[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    /// Creates a `CodecParams` for a data/subtitle stream.
348    #[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    /// Returns `true` if these are video codec parameters.
359    #[must_use]
360    pub fn is_video(&self) -> bool {
361        matches!(self.inner, CodecParamsInner::Video(_))
362    }
363
364    /// Returns `true` if these are audio codec parameters.
365    #[must_use]
366    pub fn is_audio(&self) -> bool {
367        matches!(self.inner, CodecParamsInner::Audio(_))
368    }
369
370    /// Returns a reference to the [`VideoParams`], if present.
371    #[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    /// Returns a reference to the [`AudioParams`], if present.
381    #[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    /// Builder-style setter for stream index.
391    #[must_use]
392    pub fn with_stream_index(mut self, index: u32) -> Self {
393        self.stream_index = Some(index);
394        self
395    }
396
397    /// Builder-style setter for language tag.
398    #[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// ---------------------------------------------------------------------------
406// CodecParamSet
407// ---------------------------------------------------------------------------
408
409/// A collection of [`CodecParams`] indexed by stream index, representing
410/// all streams present in a container.
411#[derive(Debug, Default, Clone)]
412pub struct CodecParamSet {
413    params: Vec<CodecParams>,
414}
415
416impl CodecParamSet {
417    /// Creates an empty `CodecParamSet`.
418    #[must_use]
419    pub fn new() -> Self {
420        Self::default()
421    }
422
423    /// Adds a [`CodecParams`] entry to the set.
424    pub fn add(&mut self, p: CodecParams) {
425        self.params.push(p);
426    }
427
428    /// Returns the number of streams.
429    #[must_use]
430    pub fn len(&self) -> usize {
431        self.params.len()
432    }
433
434    /// Returns `true` if the set contains no streams.
435    #[must_use]
436    pub fn is_empty(&self) -> bool {
437        self.params.is_empty()
438    }
439
440    /// Returns a reference to the params at position `index`, or `None`.
441    #[must_use]
442    pub fn get(&self, index: usize) -> Option<&CodecParams> {
443        self.params.get(index)
444    }
445
446    /// Returns an iterator over all params.
447    pub fn iter(&self) -> impl Iterator<Item = &CodecParams> {
448        self.params.iter()
449    }
450
451    /// Returns an iterator over video stream params.
452    pub fn video_streams(&self) -> impl Iterator<Item = &CodecParams> {
453        self.params.iter().filter(|p| p.is_video())
454    }
455
456    /// Returns an iterator over audio stream params.
457    pub fn audio_streams(&self) -> impl Iterator<Item = &CodecParams> {
458        self.params.iter().filter(|p| p.is_audio())
459    }
460
461    /// Returns the first video stream, if any.
462    #[must_use]
463    pub fn first_video(&self) -> Option<&CodecParams> {
464        self.params.iter().find(|p| p.is_video())
465    }
466
467    /// Returns the first audio stream, if any.
468    #[must_use]
469    pub fn first_audio(&self) -> Option<&CodecParams> {
470        self.params.iter().find(|p| p.is_audio())
471    }
472}
473
474// ---------------------------------------------------------------------------
475// Tests
476// ---------------------------------------------------------------------------
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::types::{CodecId, PixelFormat, Rational, SampleFormat};
482
483    // --- ColorSpace / ChromaLocation ---
484
485    #[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    // --- VideoParams ---
499
500    #[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    // --- AudioParams ---
545
546    #[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    // --- CodecParams ---
573
574    #[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    // --- CodecParamSet ---
612
613    #[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}