Skip to main content

oximedia_codec/ffv1/
decoder.rs

1//! FFV1 decoder implementation.
2//!
3//! Decodes FFV1 lossless video bitstreams as specified in RFC 9043.
4//! Supports version 3 with range coder and CRC-32 error detection.
5
6use crate::error::{CodecError, CodecResult};
7use crate::frame::{FrameType, Plane, VideoFrame};
8use crate::traits::VideoDecoder;
9use oximedia_core::{CodecId, PixelFormat, Rational, Timestamp};
10
11use super::crc32::crc32_mpeg2;
12use super::prediction::{decode_line, predict_median};
13use super::range_coder::SimpleRangeDecoder;
14use super::types::{
15    Ffv1ChromaType, Ffv1Colorspace, Ffv1Config, Ffv1Version, SliceHeader, CONTEXT_COUNT,
16    INITIAL_STATE,
17};
18
19/// FFV1 decoder.
20///
21/// Implements the `VideoDecoder` trait for decoding FFV1 lossless video.
22/// The decoder processes compressed packets and outputs decoded `VideoFrame`s.
23///
24/// # Usage
25///
26/// ```ignore
27/// use oximedia_codec::ffv1::Ffv1Decoder;
28/// use oximedia_codec::VideoDecoder;
29///
30/// let mut decoder = Ffv1Decoder::new();
31/// decoder.send_packet(&compressed_data, pts)?;
32/// if let Some(frame) = decoder.receive_frame()? {
33///     // Process decoded frame
34/// }
35/// ```
36pub struct Ffv1Decoder {
37    /// Codec configuration (parsed from extradata or first frame).
38    config: Option<Ffv1Config>,
39    /// Output frame queue.
40    output_queue: Vec<VideoFrame>,
41    /// Whether the decoder is in flush mode.
42    flushing: bool,
43    /// Number of decoded frames.
44    frame_count: u64,
45    /// Per-plane context states for range coder (reset each keyframe).
46    plane_states: Vec<Vec<u8>>,
47}
48
49impl Ffv1Decoder {
50    /// Create a new FFV1 decoder.
51    pub fn new() -> Self {
52        Self {
53            config: None,
54            output_queue: Vec::new(),
55            flushing: false,
56            frame_count: 0,
57            plane_states: Vec::new(),
58        }
59    }
60
61    /// Create a decoder initialized with extradata (configuration record).
62    pub fn with_extradata(extradata: &[u8]) -> CodecResult<Self> {
63        let mut dec = Self::new();
64        dec.parse_config(extradata)?;
65        Ok(dec)
66    }
67
68    /// Parse the FFV1 configuration record from extradata.
69    ///
70    /// For FFV1 v3, the configuration record is a range-coded bitstream
71    /// containing codec parameters. For simplicity, we also support a
72    /// compact binary format used within our own container.
73    fn parse_config(&mut self, data: &[u8]) -> CodecResult<()> {
74        // Minimal configuration record: at least 13 bytes for our binary format.
75        // Format: [version(1), colorspace(1), chroma_h_shift(1), chroma_v_shift(1),
76        //          bits(1), ec(1), num_h_slices(1), num_v_slices(1),
77        //          width(4 LE), height(4 LE)]  = 13 bytes minimum
78        if data.len() < 13 {
79            return Err(CodecError::InvalidBitstream(format!(
80                "FFV1 config too short: {} bytes, need at least 13",
81                data.len()
82            )));
83        }
84
85        let version = Ffv1Version::from_u8(data[0])?;
86        let colorspace = Ffv1Colorspace::from_u8(data[1])?;
87        let h_shift = u32::from(data[2]);
88        let v_shift = u32::from(data[3]);
89        let chroma_type = Ffv1ChromaType::from_shifts(h_shift, v_shift)?;
90        let bits_per_raw_sample = data[4];
91        let ec = data[5] != 0;
92        let num_h_slices = u32::from(data[6]);
93        let num_v_slices = u32::from(data[7]);
94
95        // Read width and height as little-endian u32
96        let width_bytes: [u8; 4] = data[8..12]
97            .try_into()
98            .map_err(|_| CodecError::InvalidBitstream("bad width bytes".to_string()))?;
99        let height_bytes_end = 12 + 4;
100        if data.len() < height_bytes_end {
101            return Err(CodecError::InvalidBitstream(
102                "FFV1 config truncated".to_string(),
103            ));
104        }
105        let height_bytes: [u8; 4] = data[12..16]
106            .try_into()
107            .map_err(|_| CodecError::InvalidBitstream("bad height bytes".to_string()))?;
108
109        let width = u32::from_le_bytes(width_bytes);
110        let height = u32::from_le_bytes(height_bytes);
111
112        let config = Ffv1Config {
113            version,
114            width,
115            height,
116            colorspace,
117            chroma_type,
118            bits_per_raw_sample,
119            num_h_slices,
120            num_v_slices,
121            ec,
122            range_coder_mode: version.uses_range_coder(),
123            state_transition_delta: Vec::new(),
124        };
125        config.validate()?;
126
127        self.init_states(&config);
128        self.config = Some(config);
129        Ok(())
130    }
131
132    /// Initialize per-plane context states.
133    fn init_states(&mut self, config: &Ffv1Config) {
134        let plane_count = config.plane_count();
135        self.plane_states.clear();
136        for _ in 0..plane_count {
137            self.plane_states.push(vec![INITIAL_STATE; CONTEXT_COUNT]);
138        }
139    }
140
141    /// Reset all context states (done at keyframes).
142    fn reset_states(&mut self) {
143        for states in &mut self.plane_states {
144            for s in states.iter_mut() {
145                *s = INITIAL_STATE;
146            }
147        }
148    }
149
150    /// Decode a complete frame from the given packet data.
151    fn decode_frame(&mut self, data: &[u8], pts: i64) -> CodecResult<VideoFrame> {
152        // Extract all needed config values upfront to avoid borrow conflicts.
153        let config = self
154            .config
155            .as_ref()
156            .ok_or_else(|| CodecError::DecoderError("FFV1 decoder not configured".to_string()))?;
157
158        let width = config.width;
159        let height = config.height;
160        let plane_count = config.plane_count();
161        let ec = config.ec;
162        let num_slices = config.num_slices();
163        let num_h_slices = config.num_h_slices;
164        let num_v_slices = config.num_v_slices;
165        let max_val = config.max_sample_value();
166
167        let plane_dims: Vec<(u32, u32)> = (0..plane_count)
168            .map(|i| config.plane_dimensions(i))
169            .collect();
170
171        // Determine pixel format
172        let pixel_format = match (
173            config.colorspace,
174            config.chroma_type,
175            config.bits_per_raw_sample,
176        ) {
177            (Ffv1Colorspace::YCbCr, Ffv1ChromaType::Chroma420, 8) => PixelFormat::Yuv420p,
178            (Ffv1Colorspace::YCbCr, Ffv1ChromaType::Chroma422, 8) => PixelFormat::Yuv422p,
179            (Ffv1Colorspace::YCbCr, Ffv1ChromaType::Chroma444, 8) => PixelFormat::Yuv444p,
180            _ => PixelFormat::Yuv420p, // fallback
181        };
182
183        // Release the immutable borrow on self.config before mutable operations.
184        let _ = config;
185
186        let is_keyframe = self.frame_count == 0;
187
188        if is_keyframe {
189            self.reset_states();
190        }
191
192        let mut frame = VideoFrame::new(pixel_format, width, height);
193        frame.timestamp = Timestamp::new(pts, Rational::new(1, 1000));
194        frame.frame_type = if is_keyframe {
195            FrameType::Key
196        } else {
197            FrameType::Inter
198        };
199
200        // Decode all planes
201        let mut planes_data: Vec<Vec<Vec<i32>>> = Vec::with_capacity(plane_count);
202
203        if num_slices == 1 {
204            // Single slice: decode all planes from the whole packet
205            let slice_data = if ec && data.len() >= 4 {
206                // Last 4 bytes are CRC
207                let payload = &data[..data.len() - 4];
208                let stored_crc_bytes: [u8; 4] = data[data.len() - 4..]
209                    .try_into()
210                    .map_err(|_| CodecError::InvalidBitstream("bad CRC bytes".to_string()))?;
211                let stored_crc = u32::from_le_bytes(stored_crc_bytes);
212                let computed_crc = crc32_mpeg2(payload);
213                if stored_crc != computed_crc {
214                    return Err(CodecError::InvalidBitstream(format!(
215                        "FFV1 slice CRC mismatch: stored={stored_crc:#010X}, computed={computed_crc:#010X}"
216                    )));
217                }
218                payload
219            } else {
220                data
221            };
222
223            for plane_idx in 0..plane_count {
224                let (pw, ph) = plane_dims[plane_idx];
225                let plane_header = SliceHeader {
226                    slice_x: 0,
227                    slice_y: 0,
228                    slice_width: pw,
229                    slice_height: ph,
230                };
231                let decoded = self.decode_slice(slice_data, &plane_header, plane_idx)?;
232                planes_data.push(decoded);
233            }
234        } else {
235            // Multi-slice: split data into slice segments
236            let slice_data_len = data.len() / (num_slices as usize);
237            for plane_idx in 0..plane_count {
238                let (pw, ph) = plane_dims[plane_idx];
239                let slice_h = ph / num_v_slices;
240                let slice_w = pw / num_h_slices;
241
242                let mut plane_lines: Vec<Vec<i32>> = Vec::new();
243
244                for sy in 0..num_v_slices {
245                    for sx in 0..num_h_slices {
246                        let slice_idx = (sy * num_h_slices + sx) as usize;
247                        let start = slice_idx * slice_data_len;
248                        let end = if slice_idx + 1 == num_slices as usize {
249                            data.len()
250                        } else {
251                            start + slice_data_len
252                        };
253                        let segment = &data[start..end];
254
255                        let sh = if sy == num_v_slices - 1 {
256                            ph - sy * slice_h
257                        } else {
258                            slice_h
259                        };
260                        let sw = if sx == num_h_slices - 1 {
261                            pw - sx * slice_w
262                        } else {
263                            slice_w
264                        };
265
266                        let header = SliceHeader {
267                            slice_x: sx * slice_w,
268                            slice_y: sy * slice_h,
269                            slice_width: sw,
270                            slice_height: sh,
271                        };
272                        let decoded = self.decode_slice(segment, &header, plane_idx)?;
273                        for line in decoded {
274                            plane_lines.push(line);
275                        }
276                    }
277                }
278                planes_data.push(plane_lines);
279            }
280        }
281
282        // Convert decoded plane data to VideoFrame planes
283        for (plane_idx, plane_lines) in planes_data.iter().enumerate() {
284            let (pw, ph) = plane_dims[plane_idx];
285            let stride = pw as usize;
286            let mut plane_data = vec![0u8; stride * ph as usize];
287
288            for (y, line) in plane_lines.iter().enumerate() {
289                if y >= ph as usize {
290                    break;
291                }
292                let row_start = y * stride;
293                for (x, &sample) in line.iter().enumerate() {
294                    if x >= pw as usize {
295                        break;
296                    }
297                    // Clamp to valid range and convert to u8
298                    let clamped = sample.clamp(0, max_val) as u8;
299                    plane_data[row_start + x] = clamped;
300                }
301            }
302
303            frame
304                .planes
305                .push(Plane::with_dimensions(plane_data, stride, pw, ph));
306        }
307
308        self.frame_count += 1;
309        Ok(frame)
310    }
311
312    /// Decode a single slice for one plane using range coder.
313    ///
314    /// Returns the decoded samples as a Vec of lines, each line being a Vec<i32>.
315    fn decode_slice(
316        &mut self,
317        data: &[u8],
318        header: &SliceHeader,
319        plane_idx: usize,
320    ) -> CodecResult<Vec<Vec<i32>>> {
321        let w = header.slice_width as usize;
322        let h = header.slice_height as usize;
323
324        if w == 0 || h == 0 {
325            return Ok(Vec::new());
326        }
327
328        if data.len() < 2 {
329            // Not enough data for range coder init; return black plane
330            let mut lines = Vec::with_capacity(h);
331            for _ in 0..h {
332                lines.push(vec![0i32; w]);
333            }
334            return Ok(lines);
335        }
336
337        let mut decoder = SimpleRangeDecoder::new(data)?;
338        let states = self
339            .plane_states
340            .get_mut(plane_idx)
341            .ok_or_else(|| CodecError::Internal("invalid plane index".to_string()))?;
342
343        let mut lines: Vec<Vec<i32>> = Vec::with_capacity(h);
344        let mut prev_line = vec![0i32; w];
345
346        for _y in 0..h {
347            let mut line = Vec::with_capacity(w);
348            for x in 0..w {
349                let residual = decoder.get_symbol(states)?;
350                let left = if x > 0 { line[x - 1] } else { 0 };
351                let top = prev_line[x];
352                let top_left = if x > 0 { prev_line[x - 1] } else { 0 };
353                let pred = predict_median(left, top, top_left);
354                line.push(pred + residual);
355            }
356            prev_line.clone_from(&line);
357            lines.push(line);
358        }
359
360        Ok(lines)
361    }
362}
363
364impl VideoDecoder for Ffv1Decoder {
365    fn codec(&self) -> CodecId {
366        CodecId::Ffv1
367    }
368
369    fn send_packet(&mut self, data: &[u8], pts: i64) -> CodecResult<()> {
370        if self.flushing {
371            return Err(CodecError::DecoderError(
372                "decoder is flushing, cannot accept new packets".to_string(),
373            ));
374        }
375
376        // If not configured yet, try to parse config from the packet.
377        // In practice, config comes from container extradata.
378        if self.config.is_none() {
379            return Err(CodecError::DecoderError(
380                "FFV1 decoder not configured: call with_extradata() first".to_string(),
381            ));
382        }
383
384        let frame = self.decode_frame(data, pts)?;
385        self.output_queue.push(frame);
386        Ok(())
387    }
388
389    fn receive_frame(&mut self) -> CodecResult<Option<VideoFrame>> {
390        if self.output_queue.is_empty() {
391            Ok(None)
392        } else {
393            Ok(Some(self.output_queue.remove(0)))
394        }
395    }
396
397    fn flush(&mut self) -> CodecResult<()> {
398        self.flushing = true;
399        Ok(())
400    }
401
402    fn reset(&mut self) {
403        self.output_queue.clear();
404        self.flushing = false;
405        self.frame_count = 0;
406        self.reset_states();
407    }
408
409    fn output_format(&self) -> Option<PixelFormat> {
410        self.config
411            .as_ref()
412            .map(|c| match (c.colorspace, c.chroma_type) {
413                (Ffv1Colorspace::YCbCr, Ffv1ChromaType::Chroma420) => PixelFormat::Yuv420p,
414                (Ffv1Colorspace::YCbCr, Ffv1ChromaType::Chroma422) => PixelFormat::Yuv422p,
415                (Ffv1Colorspace::YCbCr, Ffv1ChromaType::Chroma444) => PixelFormat::Yuv444p,
416                _ => PixelFormat::Yuv420p,
417            })
418    }
419
420    fn dimensions(&self) -> Option<(u32, u32)> {
421        self.config.as_ref().map(|c| (c.width, c.height))
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use crate::traits::VideoDecoder;
429
430    fn make_config_bytes(width: u32, height: u32) -> Vec<u8> {
431        let mut data = Vec::new();
432        data.push(3); // version = V3
433        data.push(0); // colorspace = YCbCr
434        data.push(1); // chroma h_shift = 1
435        data.push(1); // chroma v_shift = 1
436        data.push(8); // bits_per_raw_sample = 8
437        data.push(1); // ec = true
438        data.push(1); // num_h_slices
439        data.push(1); // num_v_slices
440        data.extend_from_slice(&width.to_le_bytes());
441        data.extend_from_slice(&height.to_le_bytes());
442        data
443    }
444
445    #[test]
446    #[ignore]
447    fn test_decoder_creation() {
448        let dec = Ffv1Decoder::new();
449        assert!(dec.config.is_none());
450        assert_eq!(dec.codec(), CodecId::Ffv1);
451    }
452
453    #[test]
454    #[ignore]
455    fn test_decoder_with_extradata() {
456        let config_data = make_config_bytes(320, 240);
457        let dec = Ffv1Decoder::with_extradata(&config_data).expect("valid config");
458        assert!(dec.config.is_some());
459        assert_eq!(dec.dimensions(), Some((320, 240)));
460        assert_eq!(dec.output_format(), Some(PixelFormat::Yuv420p));
461    }
462
463    #[test]
464    #[ignore]
465    fn test_decoder_invalid_config() {
466        // Too short
467        assert!(Ffv1Decoder::with_extradata(&[0; 5]).is_err());
468        // Invalid version
469        let mut bad = make_config_bytes(320, 240);
470        bad[0] = 99;
471        assert!(Ffv1Decoder::with_extradata(&bad).is_err());
472    }
473
474    #[test]
475    #[ignore]
476    fn test_decoder_not_configured() {
477        let mut dec = Ffv1Decoder::new();
478        assert!(dec.send_packet(&[0; 100], 0).is_err());
479    }
480
481    #[test]
482    #[ignore]
483    fn test_decoder_reset() {
484        let config_data = make_config_bytes(16, 16);
485        let mut dec = Ffv1Decoder::with_extradata(&config_data).expect("valid");
486        dec.frame_count = 10;
487        dec.flushing = true;
488        dec.reset();
489        assert_eq!(dec.frame_count, 0);
490        assert!(!dec.flushing);
491    }
492
493    #[test]
494    #[ignore]
495    fn test_decoder_flush() {
496        let config_data = make_config_bytes(16, 16);
497        let mut dec = Ffv1Decoder::with_extradata(&config_data).expect("valid");
498        dec.flush().expect("flush ok");
499        assert!(dec.flushing);
500        // Should reject new packets after flush
501        assert!(dec.send_packet(&[0; 100], 0).is_err());
502    }
503}