Skip to main content

oximedia_codec/ffv1/
types.rs

1//! FFV1 codec types and configuration.
2//!
3//! Defines the core types for the FFV1 (FF Video Codec 1) lossless video codec
4//! as specified in RFC 9043 / ISO/IEC 24114.
5
6use crate::error::{CodecError, CodecResult};
7
8/// FFV1 codec version.
9///
10/// FFV1 has evolved through several versions, with Version 3 being the most
11/// widely used and the version standardized in RFC 9043.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Ffv1Version {
14    /// Version 0 - original, uses Golomb-Rice coding.
15    V0,
16    /// Version 1 - adds non-planar colorspace support, uses Golomb-Rice coding.
17    V1,
18    /// Version 2 - experimental (rarely used).
19    V2,
20    /// Version 3 - RFC 9043 standard, range coder, slice-level CRC.
21    V3,
22}
23
24impl Ffv1Version {
25    /// Parse version number from integer.
26    pub fn from_u8(v: u8) -> CodecResult<Self> {
27        match v {
28            0 => Ok(Self::V0),
29            1 => Ok(Self::V1),
30            2 => Ok(Self::V2),
31            3 => Ok(Self::V3),
32            _ => Err(CodecError::InvalidParameter(format!(
33                "unsupported FFV1 version: {v}"
34            ))),
35        }
36    }
37
38    /// Convert to integer representation.
39    #[must_use]
40    pub const fn as_u8(self) -> u8 {
41        match self {
42            Self::V0 => 0,
43            Self::V1 => 1,
44            Self::V2 => 2,
45            Self::V3 => 3,
46        }
47    }
48
49    /// Whether this version uses range coding (v3+) or Golomb-Rice (v0/v1).
50    #[must_use]
51    pub const fn uses_range_coder(self) -> bool {
52        matches!(self, Self::V3 | Self::V2)
53    }
54
55    /// Whether this version supports error correction (CRC per slice).
56    #[must_use]
57    pub const fn supports_ec(self) -> bool {
58        matches!(self, Self::V3)
59    }
60}
61
62/// FFV1 colorspace.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum Ffv1Colorspace {
65    /// YCbCr colorspace (planar).
66    YCbCr,
67    /// RGB colorspace (encoded as JPEG2000-RCT transformed planes).
68    Rgb,
69}
70
71impl Ffv1Colorspace {
72    /// Parse from integer value in the bitstream.
73    pub fn from_u8(v: u8) -> CodecResult<Self> {
74        match v {
75            0 => Ok(Self::YCbCr),
76            1 => Ok(Self::Rgb),
77            _ => Err(CodecError::InvalidParameter(format!(
78                "unsupported FFV1 colorspace: {v}"
79            ))),
80        }
81    }
82
83    /// Convert to integer representation.
84    #[must_use]
85    pub const fn as_u8(self) -> u8 {
86        match self {
87            Self::YCbCr => 0,
88            Self::Rgb => 1,
89        }
90    }
91
92    /// Number of planes for this colorspace.
93    #[must_use]
94    pub const fn plane_count(self) -> usize {
95        match self {
96            Self::YCbCr => 3,
97            // RGB is encoded as 3 planes (G, B-G, R-G) + optional alpha
98            Self::Rgb => 3,
99        }
100    }
101}
102
103/// FFV1 chroma subsampling type (only for YCbCr).
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum Ffv1ChromaType {
106    /// 4:2:0 chroma subsampling.
107    Chroma420,
108    /// 4:2:2 chroma subsampling.
109    Chroma422,
110    /// 4:4:4 chroma subsampling (no subsampling).
111    Chroma444,
112}
113
114impl Ffv1ChromaType {
115    /// Horizontal subsampling shift (log2 of ratio).
116    #[must_use]
117    pub const fn h_shift(self) -> u32 {
118        match self {
119            Self::Chroma420 | Self::Chroma422 => 1,
120            Self::Chroma444 => 0,
121        }
122    }
123
124    /// Vertical subsampling shift (log2 of ratio).
125    #[must_use]
126    pub const fn v_shift(self) -> u32 {
127        match self {
128            Self::Chroma420 => 1,
129            Self::Chroma422 | Self::Chroma444 => 0,
130        }
131    }
132
133    /// Parse from horizontal and vertical chroma shifts.
134    pub fn from_shifts(h_shift: u32, v_shift: u32) -> CodecResult<Self> {
135        match (h_shift, v_shift) {
136            (1, 1) => Ok(Self::Chroma420),
137            (1, 0) => Ok(Self::Chroma422),
138            (0, 0) => Ok(Self::Chroma444),
139            _ => Err(CodecError::InvalidParameter(format!(
140                "unsupported chroma subsampling: h_shift={h_shift}, v_shift={v_shift}"
141            ))),
142        }
143    }
144}
145
146/// FFV1 configuration record.
147///
148/// Contains all parameters needed to initialize the encoder or decoder.
149/// In a container, this is stored as codec extradata (the "configuration record").
150#[derive(Clone, Debug)]
151pub struct Ffv1Config {
152    /// FFV1 version.
153    pub version: Ffv1Version,
154    /// Frame width in pixels.
155    pub width: u32,
156    /// Frame height in pixels.
157    pub height: u32,
158    /// Colorspace.
159    pub colorspace: Ffv1Colorspace,
160    /// Chroma subsampling (only meaningful for YCbCr).
161    pub chroma_type: Ffv1ChromaType,
162    /// Bits per raw sample (8, 10, 12, or 16).
163    pub bits_per_raw_sample: u8,
164    /// Number of horizontal slices.
165    pub num_h_slices: u32,
166    /// Number of vertical slices.
167    pub num_v_slices: u32,
168    /// Error correction enabled (CRC32 per slice, v3+ only).
169    pub ec: bool,
170    /// Range coder state transition table index (0 = default).
171    pub state_transition_delta: Vec<i16>,
172    /// Whether to use range coder (true) or Golomb-Rice (false).
173    /// For v3 this is always true.
174    pub range_coder_mode: bool,
175}
176
177impl Default for Ffv1Config {
178    fn default() -> Self {
179        Self {
180            version: Ffv1Version::V3,
181            width: 0,
182            height: 0,
183            colorspace: Ffv1Colorspace::YCbCr,
184            chroma_type: Ffv1ChromaType::Chroma420,
185            bits_per_raw_sample: 8,
186            num_h_slices: 1,
187            num_v_slices: 1,
188            ec: true,
189            state_transition_delta: Vec::new(),
190            range_coder_mode: true,
191        }
192    }
193}
194
195impl Ffv1Config {
196    /// Total number of slices.
197    #[must_use]
198    pub fn num_slices(&self) -> u32 {
199        self.num_h_slices * self.num_v_slices
200    }
201
202    /// Maximum sample value for the configured bit depth.
203    #[must_use]
204    pub fn max_sample_value(&self) -> i32 {
205        (1i32 << self.bits_per_raw_sample) - 1
206    }
207
208    /// Number of planes for this configuration.
209    #[must_use]
210    pub fn plane_count(&self) -> usize {
211        self.colorspace.plane_count()
212    }
213
214    /// Get the dimensions for a given plane index.
215    #[must_use]
216    pub fn plane_dimensions(&self, plane_index: usize) -> (u32, u32) {
217        if plane_index == 0 || self.colorspace == Ffv1Colorspace::Rgb {
218            (self.width, self.height)
219        } else {
220            let w =
221                (self.width + (1 << self.chroma_type.h_shift()) - 1) >> self.chroma_type.h_shift();
222            let h =
223                (self.height + (1 << self.chroma_type.v_shift()) - 1) >> self.chroma_type.v_shift();
224            (w, h)
225        }
226    }
227
228    /// Validate the configuration.
229    pub fn validate(&self) -> CodecResult<()> {
230        if self.width == 0 || self.height == 0 {
231            return Err(CodecError::InvalidParameter(
232                "frame dimensions must be nonzero".to_string(),
233            ));
234        }
235        if !matches!(self.bits_per_raw_sample, 8 | 10 | 12 | 16) {
236            return Err(CodecError::InvalidParameter(format!(
237                "unsupported bits_per_raw_sample: {}",
238                self.bits_per_raw_sample
239            )));
240        }
241        if self.num_h_slices == 0 || self.num_v_slices == 0 {
242            return Err(CodecError::InvalidParameter(
243                "slice counts must be nonzero".to_string(),
244            ));
245        }
246        Ok(())
247    }
248}
249
250/// Slice header within a frame.
251#[derive(Clone, Debug, Default)]
252pub struct SliceHeader {
253    /// X offset of this slice in pixels.
254    pub slice_x: u32,
255    /// Y offset of this slice in pixels.
256    pub slice_y: u32,
257    /// Width of this slice in pixels.
258    pub slice_width: u32,
259    /// Height of this slice in pixels.
260    pub slice_height: u32,
261}
262
263/// Number of context states used per plane line in range coder mode.
264/// Each context has an adaptive probability state (0..255).
265pub const CONTEXT_COUNT: usize = 32;
266
267/// Initial state value for range coder contexts.
268pub const INITIAL_STATE: u8 = 128;
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    #[ignore]
276    fn test_version_roundtrip() {
277        for v in [
278            Ffv1Version::V0,
279            Ffv1Version::V1,
280            Ffv1Version::V2,
281            Ffv1Version::V3,
282        ] {
283            let n = v.as_u8();
284            let parsed = Ffv1Version::from_u8(n).expect("valid version");
285            assert_eq!(parsed, v);
286        }
287    }
288
289    #[test]
290    #[ignore]
291    fn test_version_invalid() {
292        assert!(Ffv1Version::from_u8(4).is_err());
293    }
294
295    #[test]
296    #[ignore]
297    fn test_colorspace() {
298        assert_eq!(Ffv1Colorspace::YCbCr.plane_count(), 3);
299        assert_eq!(Ffv1Colorspace::Rgb.plane_count(), 3);
300        assert_eq!(Ffv1Colorspace::YCbCr.as_u8(), 0);
301        assert_eq!(Ffv1Colorspace::Rgb.as_u8(), 1);
302    }
303
304    #[test]
305    #[ignore]
306    fn test_chroma_type_shifts() {
307        assert_eq!(Ffv1ChromaType::Chroma420.h_shift(), 1);
308        assert_eq!(Ffv1ChromaType::Chroma420.v_shift(), 1);
309        assert_eq!(Ffv1ChromaType::Chroma422.h_shift(), 1);
310        assert_eq!(Ffv1ChromaType::Chroma422.v_shift(), 0);
311        assert_eq!(Ffv1ChromaType::Chroma444.h_shift(), 0);
312        assert_eq!(Ffv1ChromaType::Chroma444.v_shift(), 0);
313    }
314
315    #[test]
316    #[ignore]
317    fn test_chroma_type_from_shifts() {
318        assert_eq!(
319            Ffv1ChromaType::from_shifts(1, 1).expect("valid"),
320            Ffv1ChromaType::Chroma420
321        );
322        assert_eq!(
323            Ffv1ChromaType::from_shifts(1, 0).expect("valid"),
324            Ffv1ChromaType::Chroma422
325        );
326        assert_eq!(
327            Ffv1ChromaType::from_shifts(0, 0).expect("valid"),
328            Ffv1ChromaType::Chroma444
329        );
330        assert!(Ffv1ChromaType::from_shifts(2, 0).is_err());
331    }
332
333    #[test]
334    #[ignore]
335    fn test_config_plane_dimensions() {
336        let config = Ffv1Config {
337            width: 1920,
338            height: 1080,
339            chroma_type: Ffv1ChromaType::Chroma420,
340            ..Default::default()
341        };
342        assert_eq!(config.plane_dimensions(0), (1920, 1080));
343        assert_eq!(config.plane_dimensions(1), (960, 540));
344        assert_eq!(config.plane_dimensions(2), (960, 540));
345    }
346
347    #[test]
348    #[ignore]
349    fn test_config_validation() {
350        let mut config = Ffv1Config {
351            width: 1920,
352            height: 1080,
353            ..Default::default()
354        };
355        assert!(config.validate().is_ok());
356
357        config.width = 0;
358        assert!(config.validate().is_err());
359
360        config.width = 1920;
361        config.bits_per_raw_sample = 9;
362        assert!(config.validate().is_err());
363    }
364
365    #[test]
366    #[ignore]
367    fn test_max_sample_value() {
368        let config = Ffv1Config {
369            bits_per_raw_sample: 8,
370            ..Default::default()
371        };
372        assert_eq!(config.max_sample_value(), 255);
373
374        let config10 = Ffv1Config {
375            bits_per_raw_sample: 10,
376            ..Default::default()
377        };
378        assert_eq!(config10.max_sample_value(), 1023);
379    }
380}