Skip to main content

edgefirst_codec/video/
mod.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Hardware video encoding into, and decoding of, H.264 elementary streams.
5//!
6//! [`VideoEncoder`] compresses image tensors with the platform's video
7//! encoder, and [`VideoDecoder`] turns access units back into image tensors.
8//! On Linux both are V4L2 stateful memory-to-memory devices (i.MX 8M Plus
9//! `vsi_v4l2`, i.MX 95 Wave6, …), discovered by capability. Source tensors in
10//! DMA-BUF memory are imported without a copy, and decoded pictures are
11//! returned in the DMA-BUF buffers the device wrote; only the compressed
12//! bitstream is copied.
13//!
14//! ```rust,no_run
15//! use edgefirst_codec::video::{EncoderConfig, FrameOptions, VideoEncoder};
16//! use edgefirst_tensor::{CpuAccess, PixelFormat, TensorDyn, TensorMemory};
17//!
18//! let mut encoder = VideoEncoder::new(EncoderConfig::h264(1920, 1080, PixelFormat::Nv12, 30.0))?;
19//! let frame = TensorDyn::image(1920, 1080, PixelFormat::Nv12, edgefirst_tensor::DType::U8,
20//!                              Some(TensorMemory::DmaBuf), CpuAccess::Write)?;
21//! let mut stream = Vec::new();
22//! for pts in 0..30u64 {
23//!     let opts = FrameOptions { pts, ..FrameOptions::default() };
24//!     if let Some(au) = encoder.encode(&frame, &opts)? {
25//!         stream.extend_from_slice(&au.data);
26//!     }
27//! }
28//! for au in encoder.flush()? {
29//!     stream.extend_from_slice(&au.data);
30//! }
31//! # Ok::<(), Box<dyn std::error::Error>>(())
32//! ```
33//!
34//! Decoding takes one access unit at a time; [`access_units`] splits a byte
35//! stream into them. Each call returns everything the decoder has ready.
36//!
37//! ```rust,no_run
38//! use edgefirst_codec::video::{access_units, DecodeEvent, DecoderConfig, VideoDecoder};
39//!
40//! let stream = std::fs::read("clip.h264")?;
41//! let mut decoder = VideoDecoder::new(DecoderConfig::h264())?;
42//! let mut show = |event| match event {
43//!     DecodeEvent::Frame(frame) => println!("{} {:?}", frame.pts(), frame.tensor().shape()),
44//!     DecodeEvent::Reconfigured(info) => println!("{}x{}", info.width, info.height),
45//! };
46//! for (pts, au) in access_units(&stream).enumerate() {
47//!     decoder.decode(au, pts as u64)?.into_iter().for_each(&mut show);
48//! }
49//! decoder.flush()?.into_iter().for_each(&mut show);
50//! # Ok::<(), Box<dyn std::error::Error>>(())
51//! ```
52
53#[cfg_attr(not(all(target_os = "linux", feature = "v4l2")), allow(dead_code))] // backend use only
54pub(crate) mod h264;
55#[cfg(all(target_os = "linux", feature = "v4l2"))]
56mod v4l2;
57
58use std::sync::{Arc, Mutex};
59
60use edgefirst_tensor::{Colorimetry, PixelFormat, Region, TensorDyn};
61
62pub use h264::{access_units, AccessUnits};
63
64use crate::{CodecError, Result};
65
66/// A compressed video format.
67#[non_exhaustive]
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum VideoCodec {
70    /// ITU-T H.264 / MPEG-4 AVC, as an Annex B byte stream.
71    H264,
72}
73
74/// Bits per pixel [`Bitrate::Auto`] targets: about 10 Mbps at 1080p30.
75const AUTO_BITS_PER_PIXEL: f64 = 0.16;
76/// Lowest and highest target [`Bitrate::Auto`] chooses, in bits per second.
77const AUTO_BITRATE_RANGE: (f64, f64) = (1e6, 1e8);
78
79/// Target bitrate of an encoder. Either way the encoder uses constant-bitrate
80/// rate control where the device offers it.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum Bitrate {
83    /// A target chosen from the frame size and rate: 0.16 bits per pixel,
84    /// within 1 to 100 Mbps. See [`EncoderConfig::target_bitrate`].
85    #[default]
86    Auto,
87    /// A constant target in bits per second.
88    Bps(u32),
89}
90
91/// H.264 profile (ITU-T H.264 Annex A).
92#[non_exhaustive]
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum H264Profile {
95    /// Baseline.
96    Baseline,
97    /// Constrained Baseline.
98    ConstrainedBaseline,
99    /// Main.
100    Main,
101    /// High.
102    High,
103}
104
105/// H.264 level (ITU-T H.264 Table A-1).
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
107#[allow(missing_docs)]
108pub enum H264Level {
109    L1_0,
110    L1b,
111    L1_1,
112    L1_2,
113    L1_3,
114    L2_0,
115    L2_1,
116    L2_2,
117    L3_0,
118    L3_1,
119    L3_2,
120    L4_0,
121    L4_1,
122    L4_2,
123    L5_0,
124    L5_1,
125    L5_2,
126    L6_0,
127    L6_1,
128    L6_2,
129}
130
131impl H264Level {
132    /// Every level, lowest first.
133    const ALL: [Self; 20] = [
134        Self::L1_0,
135        Self::L1b,
136        Self::L1_1,
137        Self::L1_2,
138        Self::L1_3,
139        Self::L2_0,
140        Self::L2_1,
141        Self::L2_2,
142        Self::L3_0,
143        Self::L3_1,
144        Self::L3_2,
145        Self::L4_0,
146        Self::L4_1,
147        Self::L4_2,
148        Self::L5_0,
149        Self::L5_1,
150        Self::L5_2,
151        Self::L6_0,
152        Self::L6_1,
153        Self::L6_2,
154    ];
155
156    /// Table A-1 limits: frame size in macroblocks, macroblocks per second,
157    /// and `MaxBR`, which [`Self::lowest_for`] scales by the profile's
158    /// `cpbBrVclFactor` into bits per second.
159    const fn limits(self) -> (u64, u64, u64) {
160        match self {
161            Self::L1_0 => (99, 1_485, 64),
162            Self::L1b => (99, 1_485, 128),
163            Self::L1_1 => (396, 3_000, 192),
164            Self::L1_2 => (396, 6_000, 384),
165            Self::L1_3 => (396, 11_880, 768),
166            Self::L2_0 => (396, 11_880, 2_000),
167            Self::L2_1 => (792, 19_800, 4_000),
168            Self::L2_2 => (1_620, 20_250, 4_000),
169            Self::L3_0 => (1_620, 40_500, 10_000),
170            Self::L3_1 => (3_600, 108_000, 14_000),
171            Self::L3_2 => (5_120, 216_000, 20_000),
172            Self::L4_0 => (8_192, 245_760, 20_000),
173            Self::L4_1 => (8_192, 245_760, 50_000),
174            Self::L4_2 => (8_704, 522_240, 50_000),
175            Self::L5_0 => (22_080, 589_824, 135_000),
176            Self::L5_1 => (36_864, 983_040, 240_000),
177            Self::L5_2 => (36_864, 2_073_600, 240_000),
178            Self::L6_0 => (139_264, 4_177_920, 240_000),
179            Self::L6_1 => (139_264, 8_355_840, 480_000),
180            Self::L6_2 => (139_264, 16_711_680, 800_000),
181        }
182    }
183
184    /// The lowest level whose frame size, frame dimension, macroblock rate
185    /// and bitrate limits cover a `width`x`height` stream at `frame_rate` and
186    /// `bps` in `profile`, or `None` when even level 6.2 does not.
187    pub fn lowest_for(
188        profile: H264Profile,
189        width: u32,
190        height: u32,
191        frame_rate: f64,
192        bps: u32,
193    ) -> Option<Self> {
194        let (width_mbs, height_mbs) = (
195            u64::from(width.div_ceil(16)),
196            u64::from(height.div_ceil(16)),
197        );
198        let mbs = width_mbs * height_mbs;
199        let rate = (mbs as f64 * frame_rate).ceil();
200        // cpbBrVclFactor of Table A-2: High allows 1.25 times the bitrate.
201        let factor = match profile {
202            H264Profile::High => 1_250,
203            _ => 1_000,
204        };
205        Self::ALL.into_iter().find(|level| {
206            let (frame, mb_rate, max_br) = level.limits();
207            // A.3.1: neither dimension may exceed sqrt(8 * MaxFS) macroblocks.
208            let side = (8 * frame).isqrt();
209            mbs <= frame
210                && width_mbs <= side
211                && height_mbs <= side
212                && rate <= mb_rate as f64
213                && u64::from(bps) <= max_br * factor
214        })
215    }
216}
217
218/// Configuration of a [`VideoEncoder`].
219///
220/// Start from [`EncoderConfig::h264`] and set the fields to change.
221#[non_exhaustive]
222#[derive(Debug, Clone, PartialEq)]
223pub struct EncoderConfig {
224    /// Output codec.
225    pub codec: VideoCodec,
226    /// Width of the encoded picture in pixels.
227    pub width: u32,
228    /// Height of the encoded picture in pixels.
229    pub height: u32,
230    /// Pixel format of the source tensors.
231    pub input_format: PixelFormat,
232    /// Nominal frame rate, for rate control.
233    pub frame_rate: f64,
234    /// Target bitrate.
235    pub bitrate: Bitrate,
236    /// Frames from one key frame to the next. `None` keeps the device
237    /// default.
238    pub gop: Option<u32>,
239    /// Emit the SPS and PPS with every key frame, so a receiver can start
240    /// decoding at any key frame.
241    pub repeat_headers: bool,
242    /// Profile. `None` selects High where the device offers it, and keeps
243    /// the device default where it does not. See
244    /// [`EncoderConfig::target_profile`].
245    pub profile: Option<H264Profile>,
246    /// Level. `None` selects the lowest level that covers the stream where
247    /// the device offers it, and keeps the device default where it does not.
248    /// See [`EncoderConfig::target_level`].
249    pub level: Option<H264Level>,
250}
251
252impl EncoderConfig {
253    /// An H.264 configuration with an automatic bitrate, profile and level,
254    /// the device's default GOP, and headers repeated on every key frame.
255    pub fn h264(width: u32, height: u32, input_format: PixelFormat, frame_rate: f64) -> Self {
256        Self {
257            codec: VideoCodec::H264,
258            width,
259            height,
260            input_format,
261            frame_rate,
262            bitrate: Bitrate::Auto,
263            gop: None,
264            repeat_headers: true,
265            profile: None,
266            level: None,
267        }
268    }
269
270    /// The target bitrate in bits per second: the [`Bitrate::Bps`] value, or
271    /// for [`Bitrate::Auto`] one derived from the frame size and rate.
272    pub fn target_bitrate(&self) -> u32 {
273        match self.bitrate {
274            Bitrate::Bps(bps) => bps,
275            Bitrate::Auto => {
276                let bps = f64::from(self.width)
277                    * f64::from(self.height)
278                    * self.frame_rate
279                    * AUTO_BITS_PER_PIXEL;
280                let (lo, hi) = AUTO_BITRATE_RANGE;
281                bps.clamp(lo, hi) as u32
282            }
283        }
284    }
285
286    /// The profile to request: [`EncoderConfig::profile`], or High.
287    pub fn target_profile(&self) -> H264Profile {
288        self.profile.unwrap_or(H264Profile::High)
289    }
290
291    /// The level to request: [`EncoderConfig::level`], or the lowest that
292    /// covers the size, frame rate and [target bitrate](Self::target_bitrate)
293    /// in the [target profile](Self::target_profile). `None` when no level
294    /// covers them.
295    pub fn target_level(&self) -> Option<H264Level> {
296        self.level.or_else(|| {
297            H264Level::lowest_for(
298                self.target_profile(),
299                self.width,
300                self.height,
301                self.frame_rate,
302                self.target_bitrate(),
303            )
304        })
305    }
306
307    /// Checks the values that do not depend on a device.
308    pub fn validate(&self) -> Result<()> {
309        let invalid = |what: String| Err(CodecError::InvalidConfig(what));
310        if self.width == 0 || self.height == 0 {
311            return invalid(format!("size {}x{} is empty", self.width, self.height));
312        }
313        let (hsub, vsub) = chroma_subsampling(self.input_format);
314        if !self.width.is_multiple_of(hsub) || !self.height.is_multiple_of(vsub) {
315            return invalid(format!(
316                "size {}x{} is not a whole number of {} chroma blocks",
317                self.width, self.height, self.input_format
318            ));
319        }
320        if !self.frame_rate.is_finite() || self.frame_rate <= 0.0 {
321            return invalid(format!("frame rate {} is not positive", self.frame_rate));
322        }
323        if self.bitrate == Bitrate::Bps(0) {
324            return invalid("bitrate is 0".into());
325        }
326        if self.gop == Some(0) {
327            return invalid("GOP is 0".into());
328        }
329        Ok(())
330    }
331}
332
333/// Horizontal and vertical chroma subsampling factors of `format`.
334pub(crate) fn chroma_subsampling(format: PixelFormat) -> (u32, u32) {
335    match format {
336        PixelFormat::Nv12 => (2, 2),
337        PixelFormat::Nv16 | PixelFormat::Yuyv | PixelFormat::Vyuy => (2, 1),
338        _ => (1, 1),
339    }
340}
341
342/// Per-frame options for [`VideoEncoder::encode`].
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
344pub struct FrameOptions {
345    /// Presentation timestamp, returned unchanged on the matching
346    /// [`EncodedFrame`]. Any unit; the encoder never interprets it.
347    pub pts: u64,
348    /// The part of the source to encode, exactly the configured size.
349    /// `None` encodes the whole source, which must then be the configured
350    /// size.
351    pub crop: Option<Region>,
352    /// Encode this frame as a key frame (an IDR picture).
353    pub keyframe: bool,
354}
355
356/// One encoded access unit.
357#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct EncodedFrame {
359    /// Annex B bytes of the access unit. Key frames start with the SPS and
360    /// PPS when [`EncoderConfig::repeat_headers`] is set, and the first frame
361    /// always does.
362    pub data: Vec<u8>,
363    /// The access unit is an IDR picture.
364    pub keyframe: bool,
365    /// The [`FrameOptions::pts`] of the source frame.
366    pub pts: u64,
367}
368
369/// What a video encoder backend implements.
370pub(crate) trait EncoderBackend: Send {
371    fn encode(&mut self, src: &TensorDyn, opts: &FrameOptions) -> Result<Option<EncodedFrame>>;
372    fn flush(&mut self) -> Result<Vec<EncodedFrame>>;
373    fn device(&self) -> String;
374}
375
376/// A hardware video encoder.
377///
378/// Feed source frames with [`encode`](Self::encode), which may return the
379/// access unit of an earlier frame, and collect the rest with
380/// [`flush`](Self::flush) at the end of the stream.
381pub struct VideoEncoder {
382    backend: Box<dyn EncoderBackend>,
383}
384
385impl std::fmt::Debug for VideoEncoder {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        f.debug_struct("VideoEncoder")
388            .field("device", &self.backend.device())
389            .finish()
390    }
391}
392
393impl VideoEncoder {
394    /// Opens an encoder for `config`.
395    ///
396    /// On Linux the first V4L2 memory-to-memory device that produces the
397    /// codec and accepts the input format is used. Setting
398    /// `EDGEFIRST_CODEC_V4L2_ENCODER` to a device node uses that node only.
399    ///
400    /// # Errors
401    ///
402    /// [`CodecError::InvalidConfig`] when `config` is invalid or the device
403    /// rejects it, [`CodecError::UnsupportedFormat`] when no device accepts
404    /// the input format, and [`CodecError::NoDevice`] when there is no
405    /// encoder.
406    pub fn new(config: EncoderConfig) -> Result<Self> {
407        config.validate()?;
408        #[cfg(all(target_os = "linux", feature = "v4l2"))]
409        {
410            let backend = v4l2::encoder::V4l2Encoder::open(config)?;
411            Ok(Self {
412                backend: Box::new(backend),
413            })
414        }
415        #[cfg(not(all(target_os = "linux", feature = "v4l2")))]
416        {
417            let _ = config;
418            Err(CodecError::NoDevice(
419                "no video encoder backend in this build".into(),
420            ))
421        }
422    }
423
424    /// Encodes `src`.
425    ///
426    /// `src` must have the configured input format and, without a crop, the
427    /// configured size. DMA-BUF sources are read in place: the call returns
428    /// once the device has finished reading `src`, so the caller may reuse
429    /// it afterwards. Every source of one stream must have the same row
430    /// stride.
431    ///
432    /// Returns the next finished access unit, which may belong to an earlier
433    /// frame, or `None` when none is ready yet.
434    pub fn encode(&mut self, src: &TensorDyn, opts: &FrameOptions) -> Result<Option<EncodedFrame>> {
435        self.backend.encode(src, opts)
436    }
437
438    /// Drains the encoder: returns every access unit not yet returned, in
439    /// order. The encoder accepts new frames afterwards.
440    pub fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
441        self.backend.flush()
442    }
443
444    /// The device that encodes, for logs (a device node on Linux).
445    pub fn device(&self) -> String {
446        self.backend.device()
447    }
448}
449
450/// Where a [`VideoDecoder`] writes decoded pictures.
451#[derive(Debug)]
452pub enum OutputPool {
453    /// The decoder allocates DMA-BUF frames, and allocates them again when
454    /// the stream changes size.
455    Allocate {
456        /// Frames the caller may hold at once. The device's own minimum
457        /// (the reference pictures it keeps) and one buffer for it to
458        /// decode into are added to this.
459        count: usize,
460    },
461    /// Pictures are decoded into these DMA-BUF tensors, one buffer each.
462    /// There must be at least one more than the device keeps for reference
463    /// pictures, and each must be large enough for the stream's coded
464    /// picture at the row stride the device asks for; otherwise
465    /// [`VideoDecoder::decode`] fails with [`CodecError::InvalidConfig`] when
466    /// the stream's size becomes known. A tensor's own shape and format are
467    /// not used: the decoded frames describe the device's layout over the
468    /// same memory. Across a change of stream format the same tensors are
469    /// used again, each once the frames still showing it have dropped.
470    Provided(Vec<TensorDyn>),
471}
472
473impl Default for OutputPool {
474    fn default() -> Self {
475        Self::Allocate { count: 4 }
476    }
477}
478
479/// Configuration of a [`VideoDecoder`].
480#[non_exhaustive]
481#[derive(Debug)]
482pub struct DecoderConfig {
483    /// Input codec.
484    pub codec: VideoCodec,
485    /// Where pictures are decoded to.
486    pub output: OutputPool,
487}
488
489impl DecoderConfig {
490    /// An H.264 configuration with a decoder-allocated pool.
491    pub fn h264() -> Self {
492        Self {
493            codec: VideoCodec::H264,
494            output: OutputPool::default(),
495        }
496    }
497
498    /// Sets [`output`](Self::output).
499    pub fn with_output(mut self, output: OutputPool) -> Self {
500        self.output = output;
501        self
502    }
503}
504
505/// Geometry and format of the decoded pictures, known once the decoder has
506/// parsed the stream's headers.
507#[non_exhaustive]
508#[derive(Debug, Clone, PartialEq)]
509pub struct StreamInfo {
510    /// Width of the visible picture in pixels.
511    pub width: usize,
512    /// Height of the visible picture in pixels.
513    pub height: usize,
514    /// The visible picture within the coded one.
515    pub crop: Region,
516    /// Width of the coded picture, a whole number of macroblocks.
517    pub coded_width: usize,
518    /// Height of the coded picture, a whole number of macroblocks.
519    pub coded_height: usize,
520    /// Pixel format of the decoded frames.
521    pub format: PixelFormat,
522    /// Colorimetry the device reports, when it reports one.
523    pub colorimetry: Option<Colorimetry>,
524    /// Buffers in the output pool.
525    pub buffers: usize,
526    /// Whose memory the pictures are decoded into.
527    pub memory: OutputMemory,
528}
529
530/// Whose memory a decoder writes its pictures to. Either way the frames are
531/// DMA-BUF tensors and reach the GPU without a copy.
532#[derive(Debug, Clone, Copy, PartialEq, Eq)]
533pub enum OutputMemory {
534    /// The pool's DMA-BUFs (allocated by the decoder or provided by the
535    /// caller), imported by the device.
536    Imported,
537    /// The device's own buffers, exported as DMA-BUFs because it would not
538    /// import the pool's. CPU reads of these may be uncached and slow.
539    Exported,
540}
541
542/// What [`VideoDecoder::decode`] and [`VideoDecoder::flush`] produce, in
543/// order.
544#[derive(Debug)]
545pub enum DecodeEvent {
546    /// A decoded picture, in presentation order.
547    Frame(DecodedFrame),
548    /// The stream's size or format changed, or became known. Frames after
549    /// this have the new [`StreamInfo`]; frames already returned keep their
550    /// memory.
551    Reconfigured(StreamInfo),
552}
553
554/// Buffers that callers have dropped, for the decoder to give back to the
555/// device.
556#[derive(Debug, Default)]
557pub(crate) struct Returns(Mutex<Vec<(u64, u32)>>);
558
559impl Returns {
560    /// Takes the buffer indexes returned for pool `generation`, dropping
561    /// those of earlier pools.
562    #[cfg_attr(not(all(target_os = "linux", feature = "v4l2")), allow(dead_code))] // backend use only
563    pub(crate) fn take(&self, generation: u64) -> Vec<u32> {
564        let mut returned = self.0.lock().unwrap_or_else(|e| e.into_inner());
565        returned
566            .drain(..)
567            .filter(|&(g, _)| g == generation)
568            .map(|(_, index)| index)
569            .collect()
570    }
571}
572
573/// A decoded picture, lent from the decoder's pool.
574///
575/// The picture's buffer goes back to the device when the frame is dropped,
576/// so the tensor is only reachable through the frame: hold the frame as long
577/// as the picture is needed. To share one picture between consumers, share
578/// the frame (`Arc<DecodedFrame>`); the buffer returns when the last one
579/// drops. The pool is bounded, so frames held too long stop the decoder (see
580/// [`CodecError::PoolExhausted`]).
581#[derive(Debug)]
582pub struct DecodedFrame {
583    tensor: Arc<TensorDyn>,
584    pts: u64,
585    corrupted: bool,
586    /// Pool generation and buffer index, given back on drop.
587    slot: (u64, u32),
588    returns: Arc<Returns>,
589}
590
591impl DecodedFrame {
592    #[cfg_attr(not(all(target_os = "linux", feature = "v4l2")), allow(dead_code))] // backend use only
593    pub(crate) fn new(
594        tensor: Arc<TensorDyn>,
595        pts: u64,
596        corrupted: bool,
597        slot: (u64, u32),
598        returns: Arc<Returns>,
599    ) -> Self {
600        Self {
601            tensor,
602            pts,
603            corrupted,
604            slot,
605            returns,
606        }
607    }
608
609    /// The picture: an image tensor of the visible size in DMA-BUF memory.
610    pub fn tensor(&self) -> &TensorDyn {
611        &self.tensor
612    }
613
614    /// The `pts` passed with the access unit of this picture.
615    pub fn pts(&self) -> u64 {
616        self.pts
617    }
618
619    /// The device reported an error decoding this picture, so it may be
620    /// partly wrong (a missing reference, a damaged slice).
621    pub fn corrupted(&self) -> bool {
622        self.corrupted
623    }
624}
625
626impl Drop for DecodedFrame {
627    fn drop(&mut self) {
628        self.returns
629            .0
630            .lock()
631            .unwrap_or_else(|e| e.into_inner())
632            .push(self.slot);
633    }
634}
635
636/// What a video decoder backend implements.
637pub(crate) trait DecoderBackend: Send {
638    fn decode(&mut self, access_unit: &[u8], pts: u64) -> Result<Vec<DecodeEvent>>;
639    fn flush(&mut self) -> Result<Vec<DecodeEvent>>;
640    fn stream_info(&self) -> Option<&StreamInfo>;
641    fn device(&self) -> String;
642}
643
644/// A hardware video decoder.
645///
646/// Feed access units in decode order with [`decode`](Self::decode), which
647/// returns every picture and format change that is ready, and collect the
648/// rest with [`flush`](Self::flush) at the end of the stream.
649pub struct VideoDecoder {
650    backend: Box<dyn DecoderBackend>,
651}
652
653impl std::fmt::Debug for VideoDecoder {
654    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655        f.debug_struct("VideoDecoder")
656            .field("device", &self.backend.device())
657            .field("stream", &self.backend.stream_info())
658            .finish()
659    }
660}
661
662impl VideoDecoder {
663    /// Opens a decoder for `config`.
664    ///
665    /// On Linux the first V4L2 memory-to-memory device that decodes the
666    /// codec to NV12 is used. Setting `EDGEFIRST_CODEC_V4L2_DECODER` to a
667    /// device node uses that node only.
668    ///
669    /// # Errors
670    ///
671    /// [`CodecError::InvalidConfig`] when a provided pool is empty or not in
672    /// DMA-BUF memory, and [`CodecError::NoDevice`] when there is no decoder.
673    pub fn new(config: DecoderConfig) -> Result<Self> {
674        if let OutputPool::Provided(pool) = &config.output {
675            if pool.is_empty() {
676                return Err(CodecError::InvalidConfig(
677                    "the provided pool is empty".into(),
678                ));
679            }
680            if let Some(t) = pool
681                .iter()
682                .find(|t| t.memory() != edgefirst_tensor::TensorMemory::DmaBuf)
683            {
684                return Err(CodecError::InvalidConfig(format!(
685                    "the provided pool has a tensor in {:?} memory; the decoder writes DMA-BUF tensors",
686                    t.memory()
687                )));
688            }
689        }
690        #[cfg(all(target_os = "linux", feature = "v4l2"))]
691        {
692            let backend = v4l2::decoder::V4l2Decoder::open(config)?;
693            Ok(Self {
694                backend: Box::new(backend),
695            })
696        }
697        #[cfg(not(all(target_os = "linux", feature = "v4l2")))]
698        {
699            let _ = config;
700            Err(CodecError::NoDevice(
701                "no video decoder backend in this build".into(),
702            ))
703        }
704    }
705
706    /// Decodes one access unit (see [`access_units`]), whose picture is
707    /// tagged with `pts`.
708    ///
709    /// Returns, in order, every event ready by the time the device has taken
710    /// the access unit; it may be empty, and frames may belong to earlier
711    /// access units, since devices hold pictures back for reordering. The
712    /// first access unit yields [`DecodeEvent::Reconfigured`] once the
713    /// headers are parsed.
714    ///
715    /// # Errors
716    ///
717    /// [`CodecError::PoolExhausted`] when the caller holds the frames in
718    /// every output buffer, so the device cannot decode further; drop frames
719    /// and decode the same access unit again. [`CodecError::Timeout`] when
720    /// the device makes no progress otherwise. [`CodecError::InvalidInput`]
721    /// when the access unit is empty or larger than the device's bitstream
722    /// buffers. Events that became ready before an error are returned by
723    /// the next call.
724    pub fn decode(&mut self, access_unit: &[u8], pts: u64) -> Result<Vec<DecodeEvent>> {
725        self.backend.decode(access_unit, pts)
726    }
727
728    /// Drains the decoder: returns every event not yet returned, in order.
729    ///
730    /// The decoder accepts new access units afterwards, starting a new
731    /// sequence: the next access unit must be decodable on its own, an IDR
732    /// picture with its SPS and PPS, as at the start of a stream or after a
733    /// seek.
734    ///
735    /// # Errors
736    ///
737    /// [`CodecError::PoolExhausted`] when the caller holds the frames in
738    /// every output buffer the drain needs; drop frames and flush again.
739    /// [`CodecError::Timeout`] when the device does not finish draining.
740    pub fn flush(&mut self) -> Result<Vec<DecodeEvent>> {
741        self.backend.flush()
742    }
743
744    /// The current stream's geometry, once known.
745    pub fn stream_info(&self) -> Option<&StreamInfo> {
746        self.backend.stream_info()
747    }
748
749    /// The device that decodes, for logs (a device node on Linux).
750    pub fn device(&self) -> String {
751        self.backend.device()
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    fn nv12_1080p() -> EncoderConfig {
760        EncoderConfig::h264(1920, 1080, PixelFormat::Nv12, 30.0)
761    }
762
763    #[test]
764    fn default_h264_config_is_valid() {
765        let c = nv12_1080p();
766        assert!(c.validate().is_ok());
767        assert_eq!(c.codec, VideoCodec::H264);
768        assert_eq!(c.bitrate, Bitrate::Auto);
769        assert!(c.repeat_headers);
770        assert!(c.gop.is_none() && c.profile.is_none() && c.level.is_none());
771    }
772
773    #[test]
774    fn rejects_empty_and_odd_sizes() {
775        let mut c = nv12_1080p();
776        c.width = 0;
777        assert!(matches!(c.validate(), Err(CodecError::InvalidConfig(_))));
778        let mut c = nv12_1080p();
779        c.height = 1081;
780        assert!(matches!(c.validate(), Err(CodecError::InvalidConfig(_))));
781        let mut c = EncoderConfig::h264(641, 480, PixelFormat::Yuyv, 30.0);
782        assert!(matches!(c.validate(), Err(CodecError::InvalidConfig(_))));
783        c.width = 640;
784        c.height = 481;
785        assert!(c.validate().is_ok(), "YUYV subsamples horizontally only");
786    }
787
788    #[test]
789    fn rejects_bad_rate_bitrate_and_gop() {
790        for fps in [0.0, -1.0, f64::NAN, f64::INFINITY] {
791            let mut c = nv12_1080p();
792            c.frame_rate = fps;
793            assert!(c.validate().is_err(), "{fps}");
794        }
795        let mut c = nv12_1080p();
796        c.bitrate = Bitrate::Bps(0);
797        assert!(c.validate().is_err());
798        let mut c = nv12_1080p();
799        c.gop = Some(0);
800        assert!(c.validate().is_err());
801        c.gop = Some(1);
802        c.bitrate = Bitrate::Bps(4_000_000);
803        assert!(c.validate().is_ok());
804    }
805
806    #[test]
807    fn auto_bitrate_scales_with_size_and_rate_within_its_range() {
808        let mut c = nv12_1080p();
809        assert_eq!(c.target_bitrate(), 9_953_280);
810        c.frame_rate = 60.0;
811        assert_eq!(c.target_bitrate(), 19_906_560);
812        let c = EncoderConfig::h264(320, 240, PixelFormat::Nv12, 15.0);
813        assert_eq!(c.target_bitrate(), 1_000_000);
814        let c = EncoderConfig::h264(7680, 4320, PixelFormat::Nv12, 120.0);
815        assert_eq!(c.target_bitrate(), 100_000_000);
816        let mut c = nv12_1080p();
817        c.bitrate = Bitrate::Bps(4_000_000);
818        assert_eq!(c.target_bitrate(), 4_000_000);
819    }
820
821    #[test]
822    fn levels_cover_size_rate_and_bitrate() {
823        use H264Level as L;
824        use H264Profile::{High, Main};
825        assert_eq!(
826            L::lowest_for(High, 1920, 1080, 30.0, 10_000_000),
827            Some(L::L4_0)
828        );
829        assert_eq!(
830            L::lowest_for(High, 1920, 1080, 60.0, 20_000_000),
831            Some(L::L4_2)
832        );
833        assert_eq!(
834            L::lowest_for(High, 1920, 1080, 30.0, 40_000_000),
835            Some(L::L4_1)
836        );
837        // Main has no 1.25 bitrate allowance: 24 Mbps exceeds level 4.0's 20.
838        assert_eq!(
839            L::lowest_for(Main, 1920, 1080, 30.0, 24_000_000),
840            Some(L::L4_1)
841        );
842        assert_eq!(
843            L::lowest_for(High, 640, 480, 30.0, 2_000_000),
844            Some(L::L3_0)
845        );
846        assert_eq!(
847            L::lowest_for(High, 3840, 2160, 30.0, 40_000_000),
848            Some(L::L5_1)
849        );
850        assert_eq!(
851            L::lowest_for(High, 7680, 4320, 120.0, 100_000_000),
852            Some(L::L6_2)
853        );
854        assert_eq!(L::lowest_for(High, 7680, 4320, 240.0, 100_000_000), None);
855    }
856
857    #[test]
858    fn levels_cover_each_frame_dimension() {
859        use H264Level as L;
860        use H264Profile::High;
861        // 256 macroblocks wide fits level 3.2's frame size but not its
862        // sqrt(8 * 5120) = 202 macroblock side; level 4.0 allows 256.
863        assert_eq!(
864            L::lowest_for(High, 4096, 256, 30.0, 5_000_000),
865            Some(L::L4_0)
866        );
867        assert_eq!(
868            L::lowest_for(High, 256, 4096, 30.0, 5_000_000),
869            Some(L::L4_0)
870        );
871        // 257 macroblocks exceed level 4.1's side; 4.2 allows sqrt(8 * 8704) = 263.
872        assert_eq!(
873            L::lowest_for(High, 4112, 256, 30.0, 5_000_000),
874            Some(L::L4_2)
875        );
876    }
877
878    #[test]
879    fn unset_profile_and_level_resolve_to_high_at_the_lowest_level() {
880        let mut c = nv12_1080p();
881        assert_eq!(c.target_profile(), H264Profile::High);
882        assert_eq!(c.target_level(), Some(H264Level::L4_0));
883        c.frame_rate = 60.0;
884        assert_eq!(c.target_level(), Some(H264Level::L4_2));
885        c.profile = Some(H264Profile::Main);
886        c.level = Some(H264Level::L5_1);
887        assert_eq!(c.target_profile(), H264Profile::Main);
888        assert_eq!(c.target_level(), Some(H264Level::L5_1));
889    }
890
891    #[test]
892    fn frames_can_be_shared_between_threads() {
893        fn shareable<T: Send + Sync>() {}
894        shareable::<DecodedFrame>();
895        shareable::<Arc<DecodedFrame>>();
896    }
897
898    #[test]
899    fn a_dropped_frame_returns_its_buffer_to_its_own_pool() {
900        let returns = Arc::new(Returns::default());
901        let tensor =
902            |n| Arc::new(TensorDyn::new(&[n], edgefirst_tensor::DType::U8, None, None).unwrap());
903        let old = DecodedFrame::new(tensor(16), 7, false, (1, 3), Arc::clone(&returns));
904        let current = DecodedFrame::new(tensor(16), 8, true, (2, 5), Arc::clone(&returns));
905        assert_eq!((current.pts(), current.corrupted()), (8, true));
906        assert!(
907            returns.take(2).is_empty(),
908            "nothing returned while frames are held"
909        );
910        drop(old);
911        drop(current);
912        assert_eq!(
913            returns.take(2),
914            vec![5],
915            "a buffer of an earlier pool is not returned"
916        );
917        assert!(returns.take(1).is_empty(), "take drains every generation");
918    }
919
920    #[test]
921    fn levels_are_ordered() {
922        assert!(H264Level::L1b < H264Level::L1_1);
923        assert!(H264Level::L4_0 < H264Level::L5_1);
924    }
925}