Skip to main content

rff_codec_av2/
lib.rs

1//! In-house **AV2 decoder**, backed by the pure-Rust
2//! [`rusty_av2d`] engine — no C, no FFI.
3//!
4//! This crate is the thin `rff` adapter: [`register`] wires `rusty_av2d` into a
5//! [`CodecRegistry`], and a private wrapper translates between the rff
6//! `Decoder` trait and `rusty_av2d`'s push/pull API (`Packet`/`Frame` ↔
7//! byte buffers/planes, and the `Again` control-flow error one-to-one). All
8//! codec logic — the decoder that is byte-identical to AOM's `avmdec` across a
9//! 45-clip conformance corpus — lives in `rusty_av2d`.
10//!
11//! **Decode only.** There is no AV2 encoder; [`Codec::encoder`] is `None`.
12//!
13//! AV2 is not a finalized standard: correctness is defined against the AVM
14//! reference as of the pinned `rusty_av2d` release, and the bitstream may still
15//! change. See that crate's `STATUS.md` for the full scope statement.
16
17use rff_codec::{Codec, CodecParams, CodecRegistry, Decoder};
18use rff_core::{CodecId, Error, Frame, MediaType, Packet, PixelFormat, Result, VideoFrame};
19use rusty_av2d::{PixelLayout, PlanarImageComponent, Rav1dError};
20
21// The full engine, for callers that want the native API directly.
22pub use rusty_av2d;
23
24/// Register the AV2 decoder into a [`CodecRegistry`].
25pub fn register(registry: &mut CodecRegistry) {
26    registry.register(Codec {
27        id: CodecId::Av2,
28        name: "av2",
29        long_name: "AOMedia Video 2",
30        media_type: MediaType::Video,
31        decoder: Some(|| Box::new(Av2Decoder::new())),
32        encoder: None,
33    });
34}
35
36/// Map a `rusty_av2d` error onto the rff error space. `TryAgain` is control
37/// flow (FFmpeg's `EAGAIN` convention) and must map exactly — the send/receive
38/// loops key on it.
39fn map_err(e: Rav1dError) -> Error {
40    match e {
41        Rav1dError::TryAgain => Error::Again,
42        Rav1dError::InvalidArgument => Error::InvalidData("av2: invalid bitstream".into()),
43        Rav1dError::UnsupportedBitstream => {
44            Error::Unsupported("av2: unsupported bitstream feature".into())
45        }
46        other => Error::InvalidData(format!("av2: {other:?}")),
47    }
48}
49
50// ---------------------------------------------------------------------------
51// Decoder
52// ---------------------------------------------------------------------------
53
54struct Av2Decoder {
55    /// `None` until the first packet: constructing the engine can fail, and
56    /// [`Decoder`] construction in the registry is infallible.
57    inner: Option<rusty_av2d::Decoder>,
58    /// Set when the engine accepted only part of a packet. The engine asserts
59    /// if `send_data` is called again while data is still pending, so the
60    /// remainder must be drained through `send_pending_data` first.
61    pending: bool,
62    /// Set by [`Decoder::flush`]; makes `receive_frame` report `Eof` rather
63    /// than `Again` once the engine has no more buffered pictures.
64    draining: bool,
65}
66
67impl Av2Decoder {
68    fn new() -> Self {
69        Self {
70            inner: None,
71            pending: false,
72            draining: false,
73        }
74    }
75
76    fn engine(&mut self) -> Result<&mut rusty_av2d::Decoder> {
77        if self.inner.is_none() {
78            self.inner = Some(rusty_av2d::Decoder::new().map_err(map_err)?);
79        }
80        Ok(self.inner.as_mut().expect("just initialized"))
81    }
82}
83
84impl Decoder for Av2Decoder {
85    fn configure(&mut self, _params: &CodecParams) -> Result<()> {
86        // The AV2 bitstream carries its own size / bit-depth / colour
87        // configuration in the sequence header; container hints are not needed.
88        Ok(())
89    }
90
91    fn send_packet(&mut self, packet: &Packet) -> Result<()> {
92        let pts = packet.pts;
93
94        // Pre-validate rusty_av2d's own `sz > 0 && sz <= usize::MAX/2` input check: on a
95        // failed check its `validate_input!` calls `debug_abort()` → `process::abort()`
96        // under `debug_assertions` — an uncatchable crash (`catch_unwind` can't contain an
97        // abort). An empty packet from a truncated/hostile file trips it, so reject it here
98        // and fail gracefully in EVERY build, as the AVIF adapter does for rusty_av1d.
99        if packet.data.is_empty() || packet.data.len() > usize::MAX / 2 {
100            return Err(Error::InvalidData("av2: empty or oversized packet".into()));
101        }
102        let data = packet.data.clone().into_boxed_slice();
103
104        // Finish any partially-consumed previous packet before offering a new
105        // one — `send_data` asserts (panics) if data is still pending.
106        if self.pending {
107            let r = self.engine()?.send_pending_data();
108            match r {
109                Ok(()) => self.pending = false,
110                Err(Rav1dError::TryAgain) => return Err(Error::Again),
111                Err(e) => {
112                    self.pending = false;
113                    return Err(map_err(e));
114                }
115            }
116        }
117
118        let r = self.engine()?.send_data(data, None, pts, None);
119        match r {
120            Ok(()) => Ok(()),
121            Err(Rav1dError::TryAgain) => {
122                // Partially accepted: the engine holds the remainder. Pull
123                // frames out, then re-offer via the branch above.
124                self.pending = true;
125                Err(Error::Again)
126            }
127            Err(e) => Err(map_err(e)),
128        }
129    }
130
131    fn receive_frame(&mut self) -> Result<Frame> {
132        let draining = self.draining;
133        let Some(engine) = self.inner.as_mut() else {
134            // Nothing was ever sent.
135            return Err(if draining { Error::Eof } else { Error::Again });
136        };
137        match engine.get_picture() {
138            Ok(pic) => picture_to_frame(&pic),
139            // No picture ready: more input needed, unless we're draining, in
140            // which case the stream is done.
141            Err(Rav1dError::TryAgain) if draining => Err(Error::Eof),
142            Err(e) => Err(map_err(e)),
143        }
144    }
145
146    fn flush(&mut self) {
147        self.draining = true;
148    }
149}
150
151/// Map a `rusty_av2d` [`Picture`](rusty_av2d::Picture) onto an rff
152/// [`VideoFrame`].
153///
154/// The engine's planes are stride-padded for alignment; rff frames carry an
155/// explicit stride, so rows are passed through verbatim rather than repacked.
156fn picture_to_frame(pic: &rusty_av2d::Picture) -> Result<Frame> {
157    let layout = pic.pixel_layout();
158    let format = match (layout, pic.bit_depth()) {
159        (PixelLayout::I420, 8) => PixelFormat::Yuv420p,
160        (PixelLayout::I422, 8) => PixelFormat::Yuv422p,
161        (PixelLayout::I444, 8) => PixelFormat::Yuv444p,
162        (PixelLayout::I420, 10) => PixelFormat::Yuv420p10,
163        (PixelLayout::I422, 10) => PixelFormat::Yuv422p10,
164        (PixelLayout::I444, 10) => PixelFormat::Yuv444p10,
165        (PixelLayout::I420, 12) => PixelFormat::Yuv420p12,
166        (PixelLayout::I422, 12) => PixelFormat::Yuv422p12,
167        (PixelLayout::I444, 12) => PixelFormat::Yuv444p12,
168        // Monochrome has no rff pixel format, and no clip in the conformance
169        // corpus exercises it — refuse rather than fabricate neutral chroma.
170        (PixelLayout::I400, _) => {
171            return Err(Error::Unsupported("av2: monochrome (4:0:0) output".into()))
172        }
173        (_, depth) => {
174            return Err(Error::Unsupported(format!(
175                "av2: {depth}-bit output is not mapped to a pixel format"
176            )))
177        }
178    };
179
180    let mut planes = Vec::with_capacity(3);
181    let mut strides = Vec::with_capacity(3);
182    for c in [
183        PlanarImageComponent::Y,
184        PlanarImageComponent::U,
185        PlanarImageComponent::V,
186    ] {
187        planes.push(pic.plane(c).to_vec());
188        strides.push(pic.stride(c) as usize);
189    }
190
191    Ok(Frame::Video(VideoFrame {
192        width: pic.width(),
193        height: pic.height(),
194        format,
195        planes,
196        strides,
197        pts: pic.timestamp(),
198    }))
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn registers_a_decode_only_av2_codec() {
207        let mut reg = CodecRegistry::new();
208        register(&mut reg);
209        let codec = reg.by_id(CodecId::Av2).expect("av2 registered");
210        assert_eq!(codec.name, "av2");
211        assert_eq!(codec.media_type, MediaType::Video);
212        assert!(codec.decoder.is_some());
213        assert!(
214            codec.encoder.is_none(),
215            "there is no AV2 encoder; the registry must not advertise one"
216        );
217    }
218
219    #[test]
220    fn try_again_maps_to_the_control_flow_error() {
221        // The send/receive loops key on `Again`; if this ever maps to a plain
222        // decode error the pipeline deadlocks instead of pulling frames.
223        assert!(matches!(map_err(Rav1dError::TryAgain), Error::Again));
224    }
225
226    #[test]
227    fn garbage_input_errors_rather_than_panicking() {
228        let mut dec = Av2Decoder::new();
229        let packet = Packet {
230            data: vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0x11, 0x22, 0x33],
231            ..Default::default()
232        };
233        // Either outcome is fine — what must not happen is a panic.
234        let _ = dec.send_packet(&packet);
235        let _ = dec.receive_frame();
236    }
237}
238
239#[cfg(test)]
240mod fuzz_smoke {
241    use super::*;
242    use rff_codec::Decoder as _;
243
244    struct Rng(u64);
245    impl Rng {
246        fn next(&mut self) -> u64 {
247            self.0 ^= self.0 << 13;
248            self.0 ^= self.0 >> 7;
249            self.0 ^= self.0 << 17;
250            self.0
251        }
252        fn byte(&mut self) -> u8 {
253            (self.next() >> 33) as u8
254        }
255        fn range(&mut self, n: usize) -> usize {
256            (self.next() >> 33) as usize % n.max(1)
257        }
258    }
259
260    /// Random bytes straight into `send_packet` must never panic: the pipeline
261    /// hands the decoder whatever the container yielded, and a container can be
262    /// lying. The workspace-wide `fuzz_decode` harness caught a real underflow
263    /// here (a corrupt tile-size prefix); this keeps a fast check close to the
264    /// adapter, including the long buffers that exercise size fields.
265    #[test]
266    fn random_bytes_never_panic() {
267        let mut rng = Rng(0xFACE_B00C_1234_5678);
268        for i in 0..1500 {
269            let len = if i % 10 == 0 {
270                rng.range(65536)
271            } else {
272                rng.range(4096)
273            } + 1;
274            let data: Vec<u8> = (0..len).map(|_| rng.byte()).collect();
275            let mut dec = Av2Decoder::new();
276            let pkt = Packet {
277                data,
278                ..Default::default()
279            };
280            let _ = dec.send_packet(&pkt);
281            for _ in 0..8 {
282                if dec.receive_frame().is_err() {
283                    break;
284                }
285            }
286            dec.flush();
287            let _ = dec.receive_frame();
288        }
289    }
290
291    /// An empty packet (a truncated sample) must be a clean `Err`, not reach
292    /// rusty_av2d's input check — which `abort()`s the process in debug builds,
293    /// out of `catch_unwind`'s reach. `random_bytes_never_panic` above never
294    /// sends zero bytes, so this case needs its own test (issue #14).
295    #[test]
296    fn empty_packet_is_an_error_not_an_abort() {
297        let mut dec = Av2Decoder::new();
298        let pkt = Packet {
299            data: Vec::new(),
300            ..Default::default()
301        };
302        assert!(matches!(dec.send_packet(&pkt), Err(Error::InvalidData(_))));
303        // The decoder stays usable afterwards.
304        assert!(dec.receive_frame().is_err());
305    }
306}