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. Supports 8/10/12-bit depth.
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. Supports 8/10/12-bit depths.
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 (8-bit, 4:2:0).
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    /// Create an FFV1 encoder with explicit bit-depth and default 4:2:0 chroma.
100    ///
101    /// Validates that `bits` is one of {8, 10, 12, 16}. Frame dimensions are
102    /// taken from `config.width` / `config.height`.
103    pub fn new_with_bit_depth(config: EncoderConfig, bits: u8) -> CodecResult<Self> {
104        let ffv1_config = Ffv1Config {
105            version: Ffv1Version::V3,
106            width: config.width,
107            height: config.height,
108            colorspace: Ffv1Colorspace::YCbCr,
109            chroma_type: Ffv1ChromaType::Chroma420,
110            bits_per_raw_sample: bits,
111            num_h_slices: 1,
112            num_v_slices: 1,
113            ec: true,
114            range_coder_mode: true,
115            state_transition_delta: Vec::new(),
116        };
117        Self::with_ffv1_config(config, ffv1_config)
118    }
119
120    /// Reset all context states (done at keyframes).
121    fn reset_states(&mut self) {
122        for states in &mut self.plane_states {
123            for s in states.iter_mut() {
124                *s = INITIAL_STATE;
125            }
126        }
127    }
128
129    /// Generate the FFV1 extradata (configuration record) for the container.
130    ///
131    /// This must be stored in the container's codec private data so the
132    /// decoder can initialize correctly.
133    #[must_use]
134    pub fn extradata(&self) -> Vec<u8> {
135        let c = &self.ffv1_config;
136        let mut data = Vec::with_capacity(16);
137        data.push(c.version.as_u8());
138        data.push(c.colorspace.as_u8());
139        data.push(c.chroma_type.h_shift() as u8);
140        data.push(c.chroma_type.v_shift() as u8);
141        data.push(c.bits_per_raw_sample);
142        data.push(if c.ec { 1 } else { 0 });
143        data.push(c.num_h_slices as u8);
144        data.push(c.num_v_slices as u8);
145        data.extend_from_slice(&c.width.to_le_bytes());
146        data.extend_from_slice(&c.height.to_le_bytes());
147        data
148    }
149
150    /// Read a single sample from a plane's byte buffer, respecting bit depth.
151    ///
152    /// For 8-bit: reads one byte at `[y * stride + x]`.
153    /// For >8-bit: reads two bytes (little-endian) at `[y * stride_bytes + x * 2]`.
154    /// `stride` here is the stride in *bytes* (as stored in `Plane::stride`).
155    fn read_sample(
156        plane_data: &[u8],
157        stride: usize,
158        y: usize,
159        x: usize,
160        bps: u8,
161    ) -> CodecResult<i32> {
162        if bps <= 8 {
163            let idx = y * stride + x;
164            if idx >= plane_data.len() {
165                return Err(CodecError::InvalidBitstream(
166                    "plane data too short for 8-bit sample".to_string(),
167                ));
168            }
169            Ok(i32::from(plane_data[idx]))
170        } else {
171            // stride is in bytes; each sample occupies 2 bytes
172            let base = y * stride + x * 2;
173            if base + 1 >= plane_data.len() {
174                return Err(CodecError::InvalidBitstream(
175                    "plane data too short for 16-bit sample".to_string(),
176                ));
177            }
178            let lo = plane_data[base] as i32;
179            let hi = plane_data[base + 1] as i32;
180            Ok(lo | (hi << 8))
181        }
182    }
183
184    /// Encode a single frame into a compressed bitstream.
185    fn encode_frame(&mut self, frame: &VideoFrame) -> CodecResult<Vec<u8>> {
186        // Extract all needed config values upfront to avoid borrow conflicts.
187        let plane_count = self.ffv1_config.plane_count();
188        let cfg_width = self.ffv1_config.width;
189        let cfg_height = self.ffv1_config.height;
190        let ec = self.ffv1_config.ec;
191        let version = self.ffv1_config.version;
192        let bps = self.ffv1_config.bits_per_raw_sample;
193        let is_keyframe = self.frame_count % u64::from(self.config.keyint) == 0;
194
195        // Collect plane dimensions
196        let plane_dims: Vec<(u32, u32)> = (0..plane_count)
197            .map(|i| self.ffv1_config.plane_dimensions(i))
198            .collect();
199
200        if is_keyframe {
201            self.reset_states();
202        }
203
204        // Validate frame dimensions
205        if frame.width != cfg_width || frame.height != cfg_height {
206            return Err(CodecError::InvalidParameter(format!(
207                "frame dimensions {}x{} do not match encoder config {}x{}",
208                frame.width, frame.height, cfg_width, cfg_height
209            )));
210        }
211
212        if frame.planes.len() < plane_count {
213            return Err(CodecError::InvalidParameter(format!(
214                "frame has {} planes, need at least {}",
215                frame.planes.len(),
216                plane_count
217            )));
218        }
219
220        // Encode all planes into a single range-coded bitstream.
221        let mut encoder = SimpleRangeEncoder::new();
222
223        for plane_idx in 0..plane_count {
224            let (pw, ph) = plane_dims[plane_idx];
225            let plane = &frame.planes[plane_idx];
226            let stride = plane.stride; // bytes
227
228            let states = &mut self.plane_states[plane_idx];
229            let mut prev_line = vec![0i32; pw as usize];
230
231            for y in 0..ph as usize {
232                for x in 0..pw as usize {
233                    // Read current sample
234                    let sample = if y < plane.height as usize && x < plane.width as usize {
235                        Self::read_sample(&plane.data, stride, y, x, bps)?
236                    } else {
237                        0
238                    };
239
240                    // Read left neighbor for prediction
241                    let left = if x > 0 && y < plane.height as usize && x - 1 < plane.width as usize
242                    {
243                        Self::read_sample(&plane.data, stride, y, x - 1, bps)?
244                    } else {
245                        0
246                    };
247
248                    let top = prev_line[x];
249                    let top_left = if x > 0 { prev_line[x - 1] } else { 0 };
250
251                    let pred = predict_median(left, top, top_left);
252                    let residual = sample - pred;
253
254                    encoder.put_symbol(states, residual);
255                }
256
257                // Update prev_line with actual samples for next row's prediction
258                for x in 0..pw as usize {
259                    prev_line[x] = if y < plane.height as usize && x < plane.width as usize {
260                        Self::read_sample(&plane.data, stride, y, x, bps).unwrap_or_default()
261                    } else {
262                        0
263                    };
264                }
265            }
266        }
267
268        let mut payload = encoder.finish();
269
270        // Append CRC-32 for v3 with EC enabled
271        if ec && version == Ffv1Version::V3 {
272            let crc = crc32_mpeg2(&payload);
273            payload.extend_from_slice(&crc.to_le_bytes());
274        }
275
276        Ok(payload)
277    }
278}
279
280impl VideoEncoder for Ffv1Encoder {
281    fn codec(&self) -> CodecId {
282        CodecId::Ffv1
283    }
284
285    fn send_frame(&mut self, frame: &VideoFrame) -> CodecResult<()> {
286        if self.flushing {
287            return Err(CodecError::InvalidParameter(
288                "encoder is flushing, cannot accept new frames".to_string(),
289            ));
290        }
291
292        let pts = frame.timestamp.pts;
293        let is_keyframe = self.frame_count % u64::from(self.config.keyint) == 0;
294
295        let data = self.encode_frame(frame)?;
296
297        let packet = EncodedPacket {
298            data,
299            pts,
300            dts: pts,
301            keyframe: is_keyframe,
302            duration: None,
303        };
304
305        self.output_queue.push(packet);
306        self.frame_count += 1;
307        Ok(())
308    }
309
310    fn receive_packet(&mut self) -> CodecResult<Option<EncodedPacket>> {
311        if self.output_queue.is_empty() {
312            Ok(None)
313        } else {
314            Ok(Some(self.output_queue.remove(0)))
315        }
316    }
317
318    fn flush(&mut self) -> CodecResult<()> {
319        self.flushing = true;
320        Ok(())
321    }
322
323    fn config(&self) -> &EncoderConfig {
324        &self.config
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use crate::frame::Plane;
332    use crate::traits::VideoDecoder;
333    use oximedia_core::{PixelFormat, Rational, Timestamp};
334
335    fn make_encoder_config(width: u32, height: u32) -> EncoderConfig {
336        EncoderConfig {
337            codec: CodecId::Ffv1,
338            width,
339            height,
340            pixel_format: PixelFormat::Yuv420p,
341            framerate: Rational::new(30, 1),
342            bitrate: crate::traits::BitrateMode::Lossless,
343            preset: crate::traits::EncoderPreset::Medium,
344            profile: None,
345            keyint: 1,
346            threads: 1,
347            timebase: Rational::new(1, 1000),
348        }
349    }
350
351    fn make_test_frame(width: u32, height: u32) -> VideoFrame {
352        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
353        frame.timestamp = Timestamp::new(0, Rational::new(1, 1000));
354
355        // Y plane
356        let y_size = (width * height) as usize;
357        let mut y_data = vec![0u8; y_size];
358        for y in 0..height as usize {
359            for x in 0..width as usize {
360                y_data[y * width as usize + x] = ((x + y) % 256) as u8;
361            }
362        }
363        frame.planes.push(Plane::with_dimensions(
364            y_data,
365            width as usize,
366            width,
367            height,
368        ));
369
370        // U plane (half resolution for 4:2:0)
371        let cw = (width + 1) / 2;
372        let ch = (height + 1) / 2;
373        let u_data = vec![128u8; (cw * ch) as usize];
374        frame
375            .planes
376            .push(Plane::with_dimensions(u_data, cw as usize, cw, ch));
377
378        // V plane
379        let v_data = vec![128u8; (cw * ch) as usize];
380        frame
381            .planes
382            .push(Plane::with_dimensions(v_data, cw as usize, cw, ch));
383
384        frame
385    }
386
387    #[test]
388    #[ignore]
389    fn test_encoder_creation() {
390        let config = make_encoder_config(320, 240);
391        let enc = Ffv1Encoder::new(config).expect("valid config");
392        assert_eq!(enc.codec(), CodecId::Ffv1);
393    }
394
395    #[test]
396    #[ignore]
397    fn test_encoder_invalid_dimensions() {
398        let config = make_encoder_config(0, 240);
399        assert!(Ffv1Encoder::new(config).is_err());
400    }
401
402    #[test]
403    #[ignore]
404    fn test_encoder_extradata() {
405        let config = make_encoder_config(320, 240);
406        let enc = Ffv1Encoder::new(config).expect("valid");
407        let extra = enc.extradata();
408        assert!(extra.len() >= 13);
409        assert_eq!(extra[0], 3); // version V3
410    }
411
412    #[test]
413    #[ignore]
414    fn test_encode_single_frame() {
415        let config = make_encoder_config(16, 16);
416        let mut enc = Ffv1Encoder::new(config).expect("valid");
417        let frame = make_test_frame(16, 16);
418
419        enc.send_frame(&frame).expect("encode ok");
420        let packet = enc.receive_packet().expect("ok");
421        assert!(packet.is_some());
422        let pkt = packet.expect("packet");
423        assert!(pkt.keyframe);
424        assert!(!pkt.data.is_empty());
425    }
426
427    #[test]
428    #[ignore]
429    fn test_encode_wrong_dimensions() {
430        let config = make_encoder_config(16, 16);
431        let mut enc = Ffv1Encoder::new(config).expect("valid");
432        let frame = make_test_frame(32, 32); // wrong size
433        assert!(enc.send_frame(&frame).is_err());
434    }
435
436    #[test]
437    #[ignore]
438    fn test_encoder_flush() {
439        let config = make_encoder_config(16, 16);
440        let mut enc = Ffv1Encoder::new(config).expect("valid");
441        enc.flush().expect("flush ok");
442        let frame = make_test_frame(16, 16);
443        assert!(enc.send_frame(&frame).is_err());
444    }
445
446    #[test]
447    #[ignore]
448    fn test_lossless_roundtrip() {
449        // Encode a frame, then decode it, verify pixel-perfect roundtrip
450        let width = 16u32;
451        let height = 16u32;
452
453        let enc_config = make_encoder_config(width, height);
454        let mut encoder = Ffv1Encoder::new(enc_config).expect("enc init");
455        let frame = make_test_frame(width, height);
456
457        encoder.send_frame(&frame).expect("encode");
458        let packet = encoder.receive_packet().expect("ok").expect("has packet");
459
460        // Now decode
461        let extradata = encoder.extradata();
462        let mut decoder =
463            super::super::decoder::Ffv1Decoder::with_extradata(&extradata).expect("dec init");
464
465        decoder
466            .send_packet(&packet.data, packet.pts)
467            .expect("decode");
468        let decoded_frame = decoder.receive_frame().expect("ok").expect("has frame");
469
470        // Verify lossless: all planes must match exactly
471        assert_eq!(decoded_frame.planes.len(), frame.planes.len());
472        for (pi, (orig_plane, dec_plane)) in frame
473            .planes
474            .iter()
475            .zip(decoded_frame.planes.iter())
476            .enumerate()
477        {
478            assert_eq!(
479                orig_plane.width, dec_plane.width,
480                "plane {pi} width mismatch"
481            );
482            assert_eq!(
483                orig_plane.height, dec_plane.height,
484                "plane {pi} height mismatch"
485            );
486
487            for y in 0..orig_plane.height as usize {
488                for x in 0..orig_plane.width as usize {
489                    let orig_sample = orig_plane.data[y * orig_plane.stride + x];
490                    let dec_sample = dec_plane.data[y * dec_plane.stride + x];
491                    assert_eq!(
492                        orig_sample, dec_sample,
493                        "plane {pi} sample mismatch at ({x}, {y}): orig={orig_sample}, decoded={dec_sample}"
494                    );
495                }
496            }
497        }
498    }
499
500    #[test]
501    #[ignore]
502    fn test_lossless_roundtrip_constant_frame() {
503        let width = 8u32;
504        let height = 8u32;
505        let enc_config = make_encoder_config(width, height);
506        let mut encoder = Ffv1Encoder::new(enc_config).expect("enc init");
507
508        // Create a constant frame (all 100)
509        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
510        frame.timestamp = Timestamp::new(0, Rational::new(1, 1000));
511        let y_data = vec![100u8; (width * height) as usize];
512        frame.planes.push(Plane::with_dimensions(
513            y_data,
514            width as usize,
515            width,
516            height,
517        ));
518        let cw = (width + 1) / 2;
519        let ch = (height + 1) / 2;
520        frame.planes.push(Plane::with_dimensions(
521            vec![128u8; (cw * ch) as usize],
522            cw as usize,
523            cw,
524            ch,
525        ));
526        frame.planes.push(Plane::with_dimensions(
527            vec![128u8; (cw * ch) as usize],
528            cw as usize,
529            cw,
530            ch,
531        ));
532
533        encoder.send_frame(&frame).expect("encode");
534        let packet = encoder.receive_packet().expect("ok").expect("packet");
535
536        let extradata = encoder.extradata();
537        let mut decoder =
538            super::super::decoder::Ffv1Decoder::with_extradata(&extradata).expect("dec");
539
540        decoder.send_packet(&packet.data, 0).expect("decode");
541        let decoded = decoder.receive_frame().expect("ok").expect("frame");
542
543        for (pi, (orig, dec)) in frame.planes.iter().zip(decoded.planes.iter()).enumerate() {
544            for y in 0..orig.height as usize {
545                for x in 0..orig.width as usize {
546                    assert_eq!(
547                        orig.data[y * orig.stride + x],
548                        dec.data[y * dec.stride + x],
549                        "mismatch at plane {pi} ({x}, {y})"
550                    );
551                }
552            }
553        }
554    }
555
556    #[test]
557    #[ignore]
558    fn test_lossless_roundtrip_random_pattern() {
559        let width = 32u32;
560        let height = 32u32;
561        let enc_config = make_encoder_config(width, height);
562        let mut encoder = Ffv1Encoder::new(enc_config).expect("enc init");
563
564        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
565        frame.timestamp = Timestamp::new(1000, Rational::new(1, 1000));
566
567        // Create a more complex pattern
568        let y_size = (width * height) as usize;
569        let mut y_data = vec![0u8; y_size];
570        for i in 0..y_size {
571            // Pseudo-random but deterministic pattern
572            y_data[i] = ((i * 37 + 13) % 256) as u8;
573        }
574        frame.planes.push(Plane::with_dimensions(
575            y_data,
576            width as usize,
577            width,
578            height,
579        ));
580        let cw = (width + 1) / 2;
581        let ch = (height + 1) / 2;
582        let uv_size = (cw * ch) as usize;
583        let mut u_data = vec![0u8; uv_size];
584        let mut v_data = vec![0u8; uv_size];
585        for i in 0..uv_size {
586            u_data[i] = ((i * 53 + 7) % 256) as u8;
587            v_data[i] = ((i * 71 + 23) % 256) as u8;
588        }
589        frame
590            .planes
591            .push(Plane::with_dimensions(u_data, cw as usize, cw, ch));
592        frame
593            .planes
594            .push(Plane::with_dimensions(v_data, cw as usize, cw, ch));
595
596        encoder.send_frame(&frame).expect("encode");
597        let packet = encoder.receive_packet().expect("ok").expect("packet");
598
599        let extradata = encoder.extradata();
600        let mut decoder =
601            super::super::decoder::Ffv1Decoder::with_extradata(&extradata).expect("dec");
602
603        decoder.send_packet(&packet.data, 1000).expect("decode");
604        let decoded = decoder.receive_frame().expect("ok").expect("frame");
605
606        for (pi, (orig, dec)) in frame.planes.iter().zip(decoded.planes.iter()).enumerate() {
607            for y in 0..orig.height as usize {
608                for x in 0..orig.width as usize {
609                    assert_eq!(
610                        orig.data[y * orig.stride + x],
611                        dec.data[y * dec.stride + x],
612                        "mismatch at plane {pi} ({x}, {y})"
613                    );
614                }
615            }
616        }
617    }
618}