Skip to main content

oximedia_codec/ffv1/
encoder.rs

1//! FFV1 encoder implementation.
2//!
3//! Encodes raw video frames into FFV1 lossless bitstreams as specified
4//! in RFC 9043. Supports version 3 with range coder and CRC-32 error
5//! detection.
6
7use crate::error::{CodecError, CodecResult};
8use crate::frame::VideoFrame;
9use crate::traits::{EncodedPacket, EncoderConfig, VideoEncoder};
10use oximedia_core::CodecId;
11
12use super::crc32::crc32_mpeg2;
13use super::prediction::predict_median;
14use super::range_coder::SimpleRangeEncoder;
15use super::types::{
16    Ffv1ChromaType, Ffv1Colorspace, Ffv1Config, Ffv1Version, CONTEXT_COUNT, INITIAL_STATE,
17};
18
19/// FFV1 encoder.
20///
21/// Implements the `VideoEncoder` trait for encoding raw video frames
22/// into FFV1 lossless bitstreams.
23///
24/// # Usage
25///
26/// ```ignore
27/// use oximedia_codec::ffv1::Ffv1Encoder;
28/// use oximedia_codec::traits::EncoderConfig;
29///
30/// let config = EncoderConfig::default();
31/// let mut encoder = Ffv1Encoder::new(config)?;
32/// encoder.send_frame(&frame)?;
33/// if let Some(packet) = encoder.receive_packet()? {
34///     // Write packet to container
35/// }
36/// ```
37pub struct Ffv1Encoder {
38    /// Base encoder configuration.
39    config: EncoderConfig,
40    /// FFV1-specific configuration.
41    ffv1_config: Ffv1Config,
42    /// Output packet queue.
43    output_queue: Vec<EncodedPacket>,
44    /// Whether the encoder is in flush mode.
45    flushing: bool,
46    /// Number of encoded frames.
47    frame_count: u64,
48    /// Per-plane context states for range coder.
49    plane_states: Vec<Vec<u8>>,
50}
51
52impl Ffv1Encoder {
53    /// Create a new FFV1 encoder with default FFV1 settings.
54    pub fn new(config: EncoderConfig) -> CodecResult<Self> {
55        let ffv1_config = Ffv1Config {
56            version: Ffv1Version::V3,
57            width: config.width,
58            height: config.height,
59            colorspace: Ffv1Colorspace::YCbCr,
60            chroma_type: Ffv1ChromaType::Chroma420,
61            bits_per_raw_sample: 8,
62            num_h_slices: 1,
63            num_v_slices: 1,
64            ec: true,
65            range_coder_mode: true,
66            state_transition_delta: Vec::new(),
67        };
68        Self::with_ffv1_config(config, ffv1_config)
69    }
70
71    /// Create an FFV1 encoder with explicit FFV1 configuration.
72    pub fn with_ffv1_config(config: EncoderConfig, ffv1: Ffv1Config) -> CodecResult<Self> {
73        if config.width == 0 || config.height == 0 {
74            return Err(CodecError::InvalidParameter(
75                "frame dimensions must be nonzero".to_string(),
76            ));
77        }
78
79        let mut ffv1_config = ffv1;
80        ffv1_config.width = config.width;
81        ffv1_config.height = config.height;
82        ffv1_config.validate()?;
83
84        let plane_count = ffv1_config.plane_count();
85        let plane_states: Vec<Vec<u8>> = (0..plane_count)
86            .map(|_| vec![INITIAL_STATE; CONTEXT_COUNT])
87            .collect();
88
89        Ok(Self {
90            config,
91            ffv1_config,
92            output_queue: Vec::new(),
93            flushing: false,
94            frame_count: 0,
95            plane_states,
96        })
97    }
98
99    /// Reset all context states (done at keyframes).
100    fn reset_states(&mut self) {
101        for states in &mut self.plane_states {
102            for s in states.iter_mut() {
103                *s = INITIAL_STATE;
104            }
105        }
106    }
107
108    /// Generate the FFV1 extradata (configuration record) for the container.
109    ///
110    /// This must be stored in the container's codec private data so the
111    /// decoder can initialize correctly.
112    #[must_use]
113    pub fn extradata(&self) -> Vec<u8> {
114        let c = &self.ffv1_config;
115        let mut data = Vec::with_capacity(16);
116        data.push(c.version.as_u8());
117        data.push(c.colorspace.as_u8());
118        data.push(c.chroma_type.h_shift() as u8);
119        data.push(c.chroma_type.v_shift() as u8);
120        data.push(c.bits_per_raw_sample);
121        data.push(if c.ec { 1 } else { 0 });
122        data.push(c.num_h_slices as u8);
123        data.push(c.num_v_slices as u8);
124        data.extend_from_slice(&c.width.to_le_bytes());
125        data.extend_from_slice(&c.height.to_le_bytes());
126        data
127    }
128
129    /// Encode a single frame into a compressed bitstream.
130    fn encode_frame(&mut self, frame: &VideoFrame) -> CodecResult<Vec<u8>> {
131        // Extract all needed config values upfront to avoid borrow conflicts.
132        let plane_count = self.ffv1_config.plane_count();
133        let cfg_width = self.ffv1_config.width;
134        let cfg_height = self.ffv1_config.height;
135        let ec = self.ffv1_config.ec;
136        let version = self.ffv1_config.version;
137        let is_keyframe = self.frame_count % u64::from(self.config.keyint) == 0;
138
139        // Collect plane dimensions
140        let plane_dims: Vec<(u32, u32)> = (0..plane_count)
141            .map(|i| self.ffv1_config.plane_dimensions(i))
142            .collect();
143
144        if is_keyframe {
145            self.reset_states();
146        }
147
148        // Validate frame dimensions
149        if frame.width != cfg_width || frame.height != cfg_height {
150            return Err(CodecError::InvalidParameter(format!(
151                "frame dimensions {}x{} do not match encoder config {}x{}",
152                frame.width, frame.height, cfg_width, cfg_height
153            )));
154        }
155
156        if frame.planes.len() < plane_count {
157            return Err(CodecError::InvalidParameter(format!(
158                "frame has {} planes, need at least {}",
159                frame.planes.len(),
160                plane_count
161            )));
162        }
163
164        // Encode all planes into a single range-coded bitstream
165        let mut encoder = SimpleRangeEncoder::new();
166
167        for plane_idx in 0..plane_count {
168            let (pw, ph) = plane_dims[plane_idx];
169            let plane = &frame.planes[plane_idx];
170
171            let states = &mut self.plane_states[plane_idx];
172            let mut prev_line = vec![0i32; pw as usize];
173
174            for y in 0..ph as usize {
175                for x in 0..pw as usize {
176                    // Get sample from plane data
177                    let sample = if y < plane.height as usize && x < plane.width as usize {
178                        i32::from(plane.data[y * plane.stride + x])
179                    } else {
180                        0
181                    };
182
183                    // Compute prediction
184                    let left = if x > 0 {
185                        // Use the actual sample we just encoded for the left neighbor
186                        // (we need to track the reconstructed line)
187                        i32::from(plane.data[y * plane.stride + x - 1])
188                    } else {
189                        0
190                    };
191                    let top = prev_line[x];
192                    let top_left = if x > 0 { prev_line[x - 1] } else { 0 };
193
194                    let pred = predict_median(left, top, top_left);
195                    let residual = sample - pred;
196
197                    encoder.put_symbol(states, residual);
198
199                    // Update prev_line with actual sample for next row's prediction
200                    if x == 0 && y > 0 {
201                        // Fill prev_line from the previous row
202                    }
203                }
204
205                // Update prev_line with the current row's actual samples
206                for x in 0..pw as usize {
207                    prev_line[x] = if y < plane.height as usize && x < plane.width as usize {
208                        i32::from(plane.data[y * plane.stride + x])
209                    } else {
210                        0
211                    };
212                }
213            }
214        }
215
216        let mut payload = encoder.finish();
217
218        // Append CRC-32 for v3 with EC enabled
219        if ec && version == Ffv1Version::V3 {
220            let crc = crc32_mpeg2(&payload);
221            payload.extend_from_slice(&crc.to_le_bytes());
222        }
223
224        Ok(payload)
225    }
226}
227
228impl VideoEncoder for Ffv1Encoder {
229    fn codec(&self) -> CodecId {
230        CodecId::Ffv1
231    }
232
233    fn send_frame(&mut self, frame: &VideoFrame) -> CodecResult<()> {
234        if self.flushing {
235            return Err(CodecError::InvalidParameter(
236                "encoder is flushing, cannot accept new frames".to_string(),
237            ));
238        }
239
240        let pts = frame.timestamp.pts;
241        let is_keyframe = self.frame_count % u64::from(self.config.keyint) == 0;
242
243        let data = self.encode_frame(frame)?;
244
245        let packet = EncodedPacket {
246            data,
247            pts,
248            dts: pts,
249            keyframe: is_keyframe,
250            duration: None,
251        };
252
253        self.output_queue.push(packet);
254        self.frame_count += 1;
255        Ok(())
256    }
257
258    fn receive_packet(&mut self) -> CodecResult<Option<EncodedPacket>> {
259        if self.output_queue.is_empty() {
260            Ok(None)
261        } else {
262            Ok(Some(self.output_queue.remove(0)))
263        }
264    }
265
266    fn flush(&mut self) -> CodecResult<()> {
267        self.flushing = true;
268        Ok(())
269    }
270
271    fn config(&self) -> &EncoderConfig {
272        &self.config
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::frame::Plane;
280    use crate::traits::VideoDecoder;
281    use oximedia_core::{PixelFormat, Rational, Timestamp};
282
283    fn make_encoder_config(width: u32, height: u32) -> EncoderConfig {
284        EncoderConfig {
285            codec: CodecId::Ffv1,
286            width,
287            height,
288            pixel_format: PixelFormat::Yuv420p,
289            framerate: Rational::new(30, 1),
290            bitrate: crate::traits::BitrateMode::Lossless,
291            preset: crate::traits::EncoderPreset::Medium,
292            profile: None,
293            keyint: 1,
294            threads: 1,
295            timebase: Rational::new(1, 1000),
296        }
297    }
298
299    fn make_test_frame(width: u32, height: u32) -> VideoFrame {
300        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
301        frame.timestamp = Timestamp::new(0, Rational::new(1, 1000));
302
303        // Y plane
304        let y_size = (width * height) as usize;
305        let mut y_data = vec![0u8; y_size];
306        for y in 0..height as usize {
307            for x in 0..width as usize {
308                y_data[y * width as usize + x] = ((x + y) % 256) as u8;
309            }
310        }
311        frame.planes.push(Plane::with_dimensions(
312            y_data,
313            width as usize,
314            width,
315            height,
316        ));
317
318        // U plane (half resolution for 4:2:0)
319        let cw = (width + 1) / 2;
320        let ch = (height + 1) / 2;
321        let u_data = vec![128u8; (cw * ch) as usize];
322        frame
323            .planes
324            .push(Plane::with_dimensions(u_data, cw as usize, cw, ch));
325
326        // V plane
327        let v_data = vec![128u8; (cw * ch) as usize];
328        frame
329            .planes
330            .push(Plane::with_dimensions(v_data, cw as usize, cw, ch));
331
332        frame
333    }
334
335    #[test]
336    #[ignore]
337    fn test_encoder_creation() {
338        let config = make_encoder_config(320, 240);
339        let enc = Ffv1Encoder::new(config).expect("valid config");
340        assert_eq!(enc.codec(), CodecId::Ffv1);
341    }
342
343    #[test]
344    #[ignore]
345    fn test_encoder_invalid_dimensions() {
346        let config = make_encoder_config(0, 240);
347        assert!(Ffv1Encoder::new(config).is_err());
348    }
349
350    #[test]
351    #[ignore]
352    fn test_encoder_extradata() {
353        let config = make_encoder_config(320, 240);
354        let enc = Ffv1Encoder::new(config).expect("valid");
355        let extra = enc.extradata();
356        assert!(extra.len() >= 13);
357        assert_eq!(extra[0], 3); // version V3
358    }
359
360    #[test]
361    #[ignore]
362    fn test_encode_single_frame() {
363        let config = make_encoder_config(16, 16);
364        let mut enc = Ffv1Encoder::new(config).expect("valid");
365        let frame = make_test_frame(16, 16);
366
367        enc.send_frame(&frame).expect("encode ok");
368        let packet = enc.receive_packet().expect("ok");
369        assert!(packet.is_some());
370        let pkt = packet.expect("packet");
371        assert!(pkt.keyframe);
372        assert!(!pkt.data.is_empty());
373    }
374
375    #[test]
376    #[ignore]
377    fn test_encode_wrong_dimensions() {
378        let config = make_encoder_config(16, 16);
379        let mut enc = Ffv1Encoder::new(config).expect("valid");
380        let frame = make_test_frame(32, 32); // wrong size
381        assert!(enc.send_frame(&frame).is_err());
382    }
383
384    #[test]
385    #[ignore]
386    fn test_encoder_flush() {
387        let config = make_encoder_config(16, 16);
388        let mut enc = Ffv1Encoder::new(config).expect("valid");
389        enc.flush().expect("flush ok");
390        let frame = make_test_frame(16, 16);
391        assert!(enc.send_frame(&frame).is_err());
392    }
393
394    #[test]
395    #[ignore]
396    fn test_lossless_roundtrip() {
397        // Encode a frame, then decode it, verify pixel-perfect roundtrip
398        let width = 16u32;
399        let height = 16u32;
400
401        let enc_config = make_encoder_config(width, height);
402        let mut encoder = Ffv1Encoder::new(enc_config).expect("enc init");
403        let frame = make_test_frame(width, height);
404
405        encoder.send_frame(&frame).expect("encode");
406        let packet = encoder.receive_packet().expect("ok").expect("has packet");
407
408        // Now decode
409        let extradata = encoder.extradata();
410        let mut decoder =
411            super::super::decoder::Ffv1Decoder::with_extradata(&extradata).expect("dec init");
412
413        decoder
414            .send_packet(&packet.data, packet.pts)
415            .expect("decode");
416        let decoded_frame = decoder.receive_frame().expect("ok").expect("has frame");
417
418        // Verify lossless: all planes must match exactly
419        assert_eq!(decoded_frame.planes.len(), frame.planes.len());
420        for (pi, (orig_plane, dec_plane)) in frame
421            .planes
422            .iter()
423            .zip(decoded_frame.planes.iter())
424            .enumerate()
425        {
426            assert_eq!(
427                orig_plane.width, dec_plane.width,
428                "plane {pi} width mismatch"
429            );
430            assert_eq!(
431                orig_plane.height, dec_plane.height,
432                "plane {pi} height mismatch"
433            );
434
435            for y in 0..orig_plane.height as usize {
436                for x in 0..orig_plane.width as usize {
437                    let orig_sample = orig_plane.data[y * orig_plane.stride + x];
438                    let dec_sample = dec_plane.data[y * dec_plane.stride + x];
439                    assert_eq!(
440                        orig_sample, dec_sample,
441                        "plane {pi} sample mismatch at ({x}, {y}): orig={orig_sample}, decoded={dec_sample}"
442                    );
443                }
444            }
445        }
446    }
447
448    #[test]
449    #[ignore]
450    fn test_lossless_roundtrip_constant_frame() {
451        let width = 8u32;
452        let height = 8u32;
453        let enc_config = make_encoder_config(width, height);
454        let mut encoder = Ffv1Encoder::new(enc_config).expect("enc init");
455
456        // Create a constant frame (all 100)
457        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
458        frame.timestamp = Timestamp::new(0, Rational::new(1, 1000));
459        let y_data = vec![100u8; (width * height) as usize];
460        frame.planes.push(Plane::with_dimensions(
461            y_data,
462            width as usize,
463            width,
464            height,
465        ));
466        let cw = (width + 1) / 2;
467        let ch = (height + 1) / 2;
468        frame.planes.push(Plane::with_dimensions(
469            vec![128u8; (cw * ch) as usize],
470            cw as usize,
471            cw,
472            ch,
473        ));
474        frame.planes.push(Plane::with_dimensions(
475            vec![128u8; (cw * ch) as usize],
476            cw as usize,
477            cw,
478            ch,
479        ));
480
481        encoder.send_frame(&frame).expect("encode");
482        let packet = encoder.receive_packet().expect("ok").expect("packet");
483
484        let extradata = encoder.extradata();
485        let mut decoder =
486            super::super::decoder::Ffv1Decoder::with_extradata(&extradata).expect("dec");
487
488        decoder.send_packet(&packet.data, 0).expect("decode");
489        let decoded = decoder.receive_frame().expect("ok").expect("frame");
490
491        for (pi, (orig, dec)) in frame.planes.iter().zip(decoded.planes.iter()).enumerate() {
492            for y in 0..orig.height as usize {
493                for x in 0..orig.width as usize {
494                    assert_eq!(
495                        orig.data[y * orig.stride + x],
496                        dec.data[y * dec.stride + x],
497                        "mismatch at plane {pi} ({x}, {y})"
498                    );
499                }
500            }
501        }
502    }
503
504    #[test]
505    #[ignore]
506    fn test_lossless_roundtrip_random_pattern() {
507        let width = 32u32;
508        let height = 32u32;
509        let enc_config = make_encoder_config(width, height);
510        let mut encoder = Ffv1Encoder::new(enc_config).expect("enc init");
511
512        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
513        frame.timestamp = Timestamp::new(1000, Rational::new(1, 1000));
514
515        // Create a more complex pattern
516        let y_size = (width * height) as usize;
517        let mut y_data = vec![0u8; y_size];
518        for i in 0..y_size {
519            // Pseudo-random but deterministic pattern
520            y_data[i] = ((i * 37 + 13) % 256) as u8;
521        }
522        frame.planes.push(Plane::with_dimensions(
523            y_data,
524            width as usize,
525            width,
526            height,
527        ));
528        let cw = (width + 1) / 2;
529        let ch = (height + 1) / 2;
530        let uv_size = (cw * ch) as usize;
531        let mut u_data = vec![0u8; uv_size];
532        let mut v_data = vec![0u8; uv_size];
533        for i in 0..uv_size {
534            u_data[i] = ((i * 53 + 7) % 256) as u8;
535            v_data[i] = ((i * 71 + 23) % 256) as u8;
536        }
537        frame
538            .planes
539            .push(Plane::with_dimensions(u_data, cw as usize, cw, ch));
540        frame
541            .planes
542            .push(Plane::with_dimensions(v_data, cw as usize, cw, ch));
543
544        encoder.send_frame(&frame).expect("encode");
545        let packet = encoder.receive_packet().expect("ok").expect("packet");
546
547        let extradata = encoder.extradata();
548        let mut decoder =
549            super::super::decoder::Ffv1Decoder::with_extradata(&extradata).expect("dec");
550
551        decoder.send_packet(&packet.data, 1000).expect("decode");
552        let decoded = decoder.receive_frame().expect("ok").expect("frame");
553
554        for (pi, (orig, dec)) in frame.planes.iter().zip(decoded.planes.iter()).enumerate() {
555            for y in 0..orig.height as usize {
556                for x in 0..orig.width as usize {
557                    assert_eq!(
558                        orig.data[y * orig.stride + x],
559                        dec.data[y * dec.stride + x],
560                        "mismatch at plane {pi} ({x}, {y})"
561                    );
562                }
563            }
564        }
565    }
566}