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 bytes occupied by each sample in the output plane buffer.
209    /// Returns 1 for 8-bit depth, 2 for 10/12/16-bit depth.
210    #[must_use]
211    pub fn bytes_per_sample(&self) -> usize {
212        if self.bits_per_raw_sample <= 8 {
213            1
214        } else {
215            2
216        }
217    }
218
219    /// Number of planes for this configuration.
220    #[must_use]
221    pub fn plane_count(&self) -> usize {
222        self.colorspace.plane_count()
223    }
224
225    /// Get the dimensions for a given plane index.
226    #[must_use]
227    pub fn plane_dimensions(&self, plane_index: usize) -> (u32, u32) {
228        if plane_index == 0 || self.colorspace == Ffv1Colorspace::Rgb {
229            (self.width, self.height)
230        } else {
231            let w =
232                (self.width + (1 << self.chroma_type.h_shift()) - 1) >> self.chroma_type.h_shift();
233            let h =
234                (self.height + (1 << self.chroma_type.v_shift()) - 1) >> self.chroma_type.v_shift();
235            (w, h)
236        }
237    }
238
239    /// Validate the configuration.
240    pub fn validate(&self) -> CodecResult<()> {
241        if self.width == 0 || self.height == 0 {
242            return Err(CodecError::InvalidParameter(
243                "frame dimensions must be nonzero".to_string(),
244            ));
245        }
246        if !matches!(self.bits_per_raw_sample, 8 | 10 | 12 | 16) {
247            return Err(CodecError::InvalidParameter(format!(
248                "unsupported bits_per_raw_sample: {}",
249                self.bits_per_raw_sample
250            )));
251        }
252        if self.num_h_slices == 0 || self.num_v_slices == 0 {
253            return Err(CodecError::InvalidParameter(
254                "slice counts must be nonzero".to_string(),
255            ));
256        }
257        Ok(())
258    }
259}
260
261/// Slice header within a frame.
262#[derive(Clone, Debug, Default)]
263pub struct SliceHeader {
264    /// X offset of this slice in pixels.
265    pub slice_x: u32,
266    /// Y offset of this slice in pixels.
267    pub slice_y: u32,
268    /// Width of this slice in pixels.
269    pub slice_width: u32,
270    /// Height of this slice in pixels.
271    pub slice_height: u32,
272}
273
274/// Number of context states used per plane line in range coder mode.
275/// Each context has an adaptive probability state (0..255).
276pub const CONTEXT_COUNT: usize = 32;
277
278/// Initial state value for range coder contexts.
279pub const INITIAL_STATE: u8 = 128;
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    #[ignore]
287    fn test_version_roundtrip() {
288        for v in [
289            Ffv1Version::V0,
290            Ffv1Version::V1,
291            Ffv1Version::V2,
292            Ffv1Version::V3,
293        ] {
294            let n = v.as_u8();
295            let parsed = Ffv1Version::from_u8(n).expect("valid version");
296            assert_eq!(parsed, v);
297        }
298    }
299
300    #[test]
301    #[ignore]
302    fn test_version_invalid() {
303        assert!(Ffv1Version::from_u8(4).is_err());
304    }
305
306    #[test]
307    #[ignore]
308    fn test_colorspace() {
309        assert_eq!(Ffv1Colorspace::YCbCr.plane_count(), 3);
310        assert_eq!(Ffv1Colorspace::Rgb.plane_count(), 3);
311        assert_eq!(Ffv1Colorspace::YCbCr.as_u8(), 0);
312        assert_eq!(Ffv1Colorspace::Rgb.as_u8(), 1);
313    }
314
315    #[test]
316    #[ignore]
317    fn test_chroma_type_shifts() {
318        assert_eq!(Ffv1ChromaType::Chroma420.h_shift(), 1);
319        assert_eq!(Ffv1ChromaType::Chroma420.v_shift(), 1);
320        assert_eq!(Ffv1ChromaType::Chroma422.h_shift(), 1);
321        assert_eq!(Ffv1ChromaType::Chroma422.v_shift(), 0);
322        assert_eq!(Ffv1ChromaType::Chroma444.h_shift(), 0);
323        assert_eq!(Ffv1ChromaType::Chroma444.v_shift(), 0);
324    }
325
326    #[test]
327    #[ignore]
328    fn test_chroma_type_from_shifts() {
329        assert_eq!(
330            Ffv1ChromaType::from_shifts(1, 1).expect("valid"),
331            Ffv1ChromaType::Chroma420
332        );
333        assert_eq!(
334            Ffv1ChromaType::from_shifts(1, 0).expect("valid"),
335            Ffv1ChromaType::Chroma422
336        );
337        assert_eq!(
338            Ffv1ChromaType::from_shifts(0, 0).expect("valid"),
339            Ffv1ChromaType::Chroma444
340        );
341        assert!(Ffv1ChromaType::from_shifts(2, 0).is_err());
342    }
343
344    #[test]
345    #[ignore]
346    fn test_config_plane_dimensions() {
347        let config = Ffv1Config {
348            width: 1920,
349            height: 1080,
350            chroma_type: Ffv1ChromaType::Chroma420,
351            ..Default::default()
352        };
353        assert_eq!(config.plane_dimensions(0), (1920, 1080));
354        assert_eq!(config.plane_dimensions(1), (960, 540));
355        assert_eq!(config.plane_dimensions(2), (960, 540));
356    }
357
358    #[test]
359    #[ignore]
360    fn test_config_validation() {
361        let mut config = Ffv1Config {
362            width: 1920,
363            height: 1080,
364            ..Default::default()
365        };
366        assert!(config.validate().is_ok());
367
368        config.width = 0;
369        assert!(config.validate().is_err());
370
371        config.width = 1920;
372        config.bits_per_raw_sample = 9;
373        assert!(config.validate().is_err());
374    }
375
376    #[test]
377    #[ignore]
378    fn test_max_sample_value() {
379        let config = Ffv1Config {
380            bits_per_raw_sample: 8,
381            ..Default::default()
382        };
383        assert_eq!(config.max_sample_value(), 255);
384
385        let config10 = Ffv1Config {
386            bits_per_raw_sample: 10,
387            ..Default::default()
388        };
389        assert_eq!(config10.max_sample_value(), 1023);
390    }
391}