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/// Target bitrate of an encoder.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub enum Bitrate {
77    /// The device's default bitrate and rate control.
78    #[default]
79    Auto,
80    /// A constant target in bits per second. Selects constant-bitrate rate
81    /// control where the device offers a choice.
82    Bps(u32),
83}
84
85/// H.264 profile (ITU-T H.264 Annex A).
86#[non_exhaustive]
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88pub enum H264Profile {
89    /// Baseline.
90    Baseline,
91    /// Constrained Baseline.
92    ConstrainedBaseline,
93    /// Main.
94    Main,
95    /// High.
96    High,
97}
98
99/// H.264 level (ITU-T H.264 Table A-1).
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
101#[allow(missing_docs)]
102pub enum H264Level {
103    L1_0,
104    L1b,
105    L1_1,
106    L1_2,
107    L1_3,
108    L2_0,
109    L2_1,
110    L2_2,
111    L3_0,
112    L3_1,
113    L3_2,
114    L4_0,
115    L4_1,
116    L4_2,
117    L5_0,
118    L5_1,
119    L5_2,
120    L6_0,
121    L6_1,
122    L6_2,
123}
124
125/// Configuration of a [`VideoEncoder`].
126///
127/// Start from [`EncoderConfig::h264`] and set the fields to change.
128#[non_exhaustive]
129#[derive(Debug, Clone, PartialEq)]
130pub struct EncoderConfig {
131    /// Output codec.
132    pub codec: VideoCodec,
133    /// Width of the encoded picture in pixels.
134    pub width: u32,
135    /// Height of the encoded picture in pixels.
136    pub height: u32,
137    /// Pixel format of the source tensors.
138    pub input_format: PixelFormat,
139    /// Nominal frame rate, for rate control.
140    pub frame_rate: f64,
141    /// Target bitrate.
142    pub bitrate: Bitrate,
143    /// Frames from one key frame to the next. `None` keeps the device
144    /// default.
145    pub gop: Option<u32>,
146    /// Emit the SPS and PPS with every key frame, so a receiver can start
147    /// decoding at any key frame.
148    pub repeat_headers: bool,
149    /// Profile. `None` keeps the device default.
150    pub profile: Option<H264Profile>,
151    /// Level. `None` keeps the device default.
152    pub level: Option<H264Level>,
153}
154
155impl EncoderConfig {
156    /// An H.264 configuration with the device's default bitrate, GOP,
157    /// profile and level, and headers repeated on every key frame.
158    pub fn h264(width: u32, height: u32, input_format: PixelFormat, frame_rate: f64) -> Self {
159        Self {
160            codec: VideoCodec::H264,
161            width,
162            height,
163            input_format,
164            frame_rate,
165            bitrate: Bitrate::Auto,
166            gop: None,
167            repeat_headers: true,
168            profile: None,
169            level: None,
170        }
171    }
172
173    /// Checks the values that do not depend on a device.
174    pub fn validate(&self) -> Result<()> {
175        let invalid = |what: String| Err(CodecError::InvalidConfig(what));
176        if self.width == 0 || self.height == 0 {
177            return invalid(format!("size {}x{} is empty", self.width, self.height));
178        }
179        let (hsub, vsub) = chroma_subsampling(self.input_format);
180        if !self.width.is_multiple_of(hsub) || !self.height.is_multiple_of(vsub) {
181            return invalid(format!(
182                "size {}x{} is not a whole number of {} chroma blocks",
183                self.width, self.height, self.input_format
184            ));
185        }
186        if !self.frame_rate.is_finite() || self.frame_rate <= 0.0 {
187            return invalid(format!("frame rate {} is not positive", self.frame_rate));
188        }
189        if self.bitrate == Bitrate::Bps(0) {
190            return invalid("bitrate is 0".into());
191        }
192        if self.gop == Some(0) {
193            return invalid("GOP is 0".into());
194        }
195        Ok(())
196    }
197}
198
199/// Horizontal and vertical chroma subsampling factors of `format`.
200pub(crate) fn chroma_subsampling(format: PixelFormat) -> (u32, u32) {
201    match format {
202        PixelFormat::Nv12 => (2, 2),
203        PixelFormat::Nv16 | PixelFormat::Yuyv | PixelFormat::Vyuy => (2, 1),
204        _ => (1, 1),
205    }
206}
207
208/// Per-frame options for [`VideoEncoder::encode`].
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210pub struct FrameOptions {
211    /// Presentation timestamp, returned unchanged on the matching
212    /// [`EncodedFrame`]. Any unit; the encoder never interprets it.
213    pub pts: u64,
214    /// The part of the source to encode, exactly the configured size.
215    /// `None` encodes the whole source, which must then be the configured
216    /// size.
217    pub crop: Option<Region>,
218    /// Encode this frame as a key frame (an IDR picture).
219    pub keyframe: bool,
220}
221
222/// One encoded access unit.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct EncodedFrame {
225    /// Annex B bytes of the access unit. Key frames start with the SPS and
226    /// PPS when [`EncoderConfig::repeat_headers`] is set, and the first frame
227    /// always does.
228    pub data: Vec<u8>,
229    /// The access unit is an IDR picture.
230    pub keyframe: bool,
231    /// The [`FrameOptions::pts`] of the source frame.
232    pub pts: u64,
233}
234
235/// What a video encoder backend implements.
236pub(crate) trait EncoderBackend: Send {
237    fn encode(&mut self, src: &TensorDyn, opts: &FrameOptions) -> Result<Option<EncodedFrame>>;
238    fn flush(&mut self) -> Result<Vec<EncodedFrame>>;
239    fn device(&self) -> String;
240}
241
242/// A hardware video encoder.
243///
244/// Feed source frames with [`encode`](Self::encode), which may return the
245/// access unit of an earlier frame, and collect the rest with
246/// [`flush`](Self::flush) at the end of the stream.
247pub struct VideoEncoder {
248    backend: Box<dyn EncoderBackend>,
249}
250
251impl std::fmt::Debug for VideoEncoder {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.debug_struct("VideoEncoder")
254            .field("device", &self.backend.device())
255            .finish()
256    }
257}
258
259impl VideoEncoder {
260    /// Opens an encoder for `config`.
261    ///
262    /// On Linux the first V4L2 memory-to-memory device that produces the
263    /// codec and accepts the input format is used. Setting
264    /// `EDGEFIRST_CODEC_V4L2_ENCODER` to a device node uses that node only.
265    ///
266    /// # Errors
267    ///
268    /// [`CodecError::InvalidConfig`] when `config` is invalid or the device
269    /// rejects it, [`CodecError::UnsupportedFormat`] when no device accepts
270    /// the input format, and [`CodecError::NoDevice`] when there is no
271    /// encoder.
272    pub fn new(config: EncoderConfig) -> Result<Self> {
273        config.validate()?;
274        #[cfg(all(target_os = "linux", feature = "v4l2"))]
275        {
276            let backend = v4l2::encoder::V4l2Encoder::open(config)?;
277            Ok(Self {
278                backend: Box::new(backend),
279            })
280        }
281        #[cfg(not(all(target_os = "linux", feature = "v4l2")))]
282        {
283            let _ = config;
284            Err(CodecError::NoDevice(
285                "no video encoder backend in this build".into(),
286            ))
287        }
288    }
289
290    /// Encodes `src`.
291    ///
292    /// `src` must have the configured input format and, without a crop, the
293    /// configured size. DMA-BUF sources are read in place: the call returns
294    /// once the device has finished reading `src`, so the caller may reuse
295    /// it afterwards. Every source of one stream must have the same row
296    /// stride.
297    ///
298    /// Returns the next finished access unit, which may belong to an earlier
299    /// frame, or `None` when none is ready yet.
300    pub fn encode(&mut self, src: &TensorDyn, opts: &FrameOptions) -> Result<Option<EncodedFrame>> {
301        self.backend.encode(src, opts)
302    }
303
304    /// Drains the encoder: returns every access unit not yet returned, in
305    /// order. The encoder accepts new frames afterwards.
306    pub fn flush(&mut self) -> Result<Vec<EncodedFrame>> {
307        self.backend.flush()
308    }
309
310    /// The device that encodes, for logs (a device node on Linux).
311    pub fn device(&self) -> String {
312        self.backend.device()
313    }
314}
315
316/// Where a [`VideoDecoder`] writes decoded pictures.
317#[derive(Debug)]
318pub enum OutputPool {
319    /// The decoder allocates DMA-BUF frames, and allocates them again when
320    /// the stream changes size.
321    Allocate {
322        /// Frames the caller may hold at once. The device's own minimum
323        /// (the reference pictures it keeps) and one buffer for it to
324        /// decode into are added to this.
325        count: usize,
326    },
327    /// Pictures are decoded into these DMA-BUF tensors, one buffer each.
328    /// There must be at least one more than the device keeps for reference
329    /// pictures, and each must be large enough for the stream's coded
330    /// picture at the row stride the device asks for; otherwise
331    /// [`VideoDecoder::decode`] fails with [`CodecError::InvalidConfig`] when
332    /// the stream's size becomes known. A tensor's own shape and format are
333    /// not used: the decoded frames describe the device's layout over the
334    /// same memory. Across a change of stream format the same tensors are
335    /// used again, each once the frames still showing it have dropped.
336    Provided(Vec<TensorDyn>),
337}
338
339impl Default for OutputPool {
340    fn default() -> Self {
341        Self::Allocate { count: 4 }
342    }
343}
344
345/// Configuration of a [`VideoDecoder`].
346#[non_exhaustive]
347#[derive(Debug)]
348pub struct DecoderConfig {
349    /// Input codec.
350    pub codec: VideoCodec,
351    /// Where pictures are decoded to.
352    pub output: OutputPool,
353}
354
355impl DecoderConfig {
356    /// An H.264 configuration with a decoder-allocated pool.
357    pub fn h264() -> Self {
358        Self {
359            codec: VideoCodec::H264,
360            output: OutputPool::default(),
361        }
362    }
363
364    /// Sets [`output`](Self::output).
365    pub fn with_output(mut self, output: OutputPool) -> Self {
366        self.output = output;
367        self
368    }
369}
370
371/// Geometry and format of the decoded pictures, known once the decoder has
372/// parsed the stream's headers.
373#[non_exhaustive]
374#[derive(Debug, Clone, PartialEq)]
375pub struct StreamInfo {
376    /// Width of the visible picture in pixels.
377    pub width: usize,
378    /// Height of the visible picture in pixels.
379    pub height: usize,
380    /// The visible picture within the coded one.
381    pub crop: Region,
382    /// Width of the coded picture, a whole number of macroblocks.
383    pub coded_width: usize,
384    /// Height of the coded picture, a whole number of macroblocks.
385    pub coded_height: usize,
386    /// Pixel format of the decoded frames.
387    pub format: PixelFormat,
388    /// Colorimetry the device reports, when it reports one.
389    pub colorimetry: Option<Colorimetry>,
390    /// Buffers in the output pool.
391    pub buffers: usize,
392    /// Whose memory the pictures are decoded into.
393    pub memory: OutputMemory,
394}
395
396/// Whose memory a decoder writes its pictures to. Either way the frames are
397/// DMA-BUF tensors and reach the GPU without a copy.
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum OutputMemory {
400    /// The pool's DMA-BUFs (allocated by the decoder or provided by the
401    /// caller), imported by the device.
402    Imported,
403    /// The device's own buffers, exported as DMA-BUFs because it would not
404    /// import the pool's. CPU reads of these may be uncached and slow.
405    Exported,
406}
407
408/// What [`VideoDecoder::decode`] and [`VideoDecoder::flush`] produce, in
409/// order.
410#[derive(Debug)]
411pub enum DecodeEvent {
412    /// A decoded picture, in presentation order.
413    Frame(DecodedFrame),
414    /// The stream's size or format changed, or became known. Frames after
415    /// this have the new [`StreamInfo`]; frames already returned keep their
416    /// memory.
417    Reconfigured(StreamInfo),
418}
419
420/// Buffers that callers have dropped, for the decoder to give back to the
421/// device.
422#[derive(Debug, Default)]
423pub(crate) struct Returns(Mutex<Vec<(u64, u32)>>);
424
425impl Returns {
426    /// Takes the buffer indexes returned for pool `generation`, dropping
427    /// those of earlier pools.
428    #[cfg_attr(not(all(target_os = "linux", feature = "v4l2")), allow(dead_code))] // backend use only
429    pub(crate) fn take(&self, generation: u64) -> Vec<u32> {
430        let mut returned = self.0.lock().unwrap_or_else(|e| e.into_inner());
431        returned
432            .drain(..)
433            .filter(|&(g, _)| g == generation)
434            .map(|(_, index)| index)
435            .collect()
436    }
437}
438
439/// A decoded picture, lent from the decoder's pool.
440///
441/// The picture's buffer goes back to the device when the frame is dropped,
442/// so the tensor is only reachable through the frame: hold the frame as long
443/// as the picture is needed. To share one picture between consumers, share
444/// the frame (`Arc<DecodedFrame>`); the buffer returns when the last one
445/// drops. The pool is bounded, so frames held too long stop the decoder (see
446/// [`CodecError::PoolExhausted`]).
447#[derive(Debug)]
448pub struct DecodedFrame {
449    tensor: Arc<TensorDyn>,
450    pts: u64,
451    corrupted: bool,
452    /// Pool generation and buffer index, given back on drop.
453    slot: (u64, u32),
454    returns: Arc<Returns>,
455}
456
457impl DecodedFrame {
458    #[cfg_attr(not(all(target_os = "linux", feature = "v4l2")), allow(dead_code))] // backend use only
459    pub(crate) fn new(
460        tensor: Arc<TensorDyn>,
461        pts: u64,
462        corrupted: bool,
463        slot: (u64, u32),
464        returns: Arc<Returns>,
465    ) -> Self {
466        Self {
467            tensor,
468            pts,
469            corrupted,
470            slot,
471            returns,
472        }
473    }
474
475    /// The picture: an image tensor of the visible size in DMA-BUF memory.
476    pub fn tensor(&self) -> &TensorDyn {
477        &self.tensor
478    }
479
480    /// The `pts` passed with the access unit of this picture.
481    pub fn pts(&self) -> u64 {
482        self.pts
483    }
484
485    /// The device reported an error decoding this picture, so it may be
486    /// partly wrong (a missing reference, a damaged slice).
487    pub fn corrupted(&self) -> bool {
488        self.corrupted
489    }
490}
491
492impl Drop for DecodedFrame {
493    fn drop(&mut self) {
494        self.returns
495            .0
496            .lock()
497            .unwrap_or_else(|e| e.into_inner())
498            .push(self.slot);
499    }
500}
501
502/// What a video decoder backend implements.
503pub(crate) trait DecoderBackend: Send {
504    fn decode(&mut self, access_unit: &[u8], pts: u64) -> Result<Vec<DecodeEvent>>;
505    fn flush(&mut self) -> Result<Vec<DecodeEvent>>;
506    fn stream_info(&self) -> Option<&StreamInfo>;
507    fn device(&self) -> String;
508}
509
510/// A hardware video decoder.
511///
512/// Feed access units in decode order with [`decode`](Self::decode), which
513/// returns every picture and format change that is ready, and collect the
514/// rest with [`flush`](Self::flush) at the end of the stream.
515pub struct VideoDecoder {
516    backend: Box<dyn DecoderBackend>,
517}
518
519impl std::fmt::Debug for VideoDecoder {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        f.debug_struct("VideoDecoder")
522            .field("device", &self.backend.device())
523            .field("stream", &self.backend.stream_info())
524            .finish()
525    }
526}
527
528impl VideoDecoder {
529    /// Opens a decoder for `config`.
530    ///
531    /// On Linux the first V4L2 memory-to-memory device that decodes the
532    /// codec to NV12 is used. Setting `EDGEFIRST_CODEC_V4L2_DECODER` to a
533    /// device node uses that node only.
534    ///
535    /// # Errors
536    ///
537    /// [`CodecError::InvalidConfig`] when a provided pool is empty or not in
538    /// DMA-BUF memory, and [`CodecError::NoDevice`] when there is no decoder.
539    pub fn new(config: DecoderConfig) -> Result<Self> {
540        if let OutputPool::Provided(pool) = &config.output {
541            if pool.is_empty() {
542                return Err(CodecError::InvalidConfig(
543                    "the provided pool is empty".into(),
544                ));
545            }
546            if let Some(t) = pool
547                .iter()
548                .find(|t| t.memory() != edgefirst_tensor::TensorMemory::DmaBuf)
549            {
550                return Err(CodecError::InvalidConfig(format!(
551                    "the provided pool has a tensor in {:?} memory; the decoder writes DMA-BUF tensors",
552                    t.memory()
553                )));
554            }
555        }
556        #[cfg(all(target_os = "linux", feature = "v4l2"))]
557        {
558            let backend = v4l2::decoder::V4l2Decoder::open(config)?;
559            Ok(Self {
560                backend: Box::new(backend),
561            })
562        }
563        #[cfg(not(all(target_os = "linux", feature = "v4l2")))]
564        {
565            let _ = config;
566            Err(CodecError::NoDevice(
567                "no video decoder backend in this build".into(),
568            ))
569        }
570    }
571
572    /// Decodes one access unit (see [`access_units`]), whose picture is
573    /// tagged with `pts`.
574    ///
575    /// Returns, in order, every event ready by the time the device has taken
576    /// the access unit; it may be empty, and frames may belong to earlier
577    /// access units, since devices hold pictures back for reordering. The
578    /// first access unit yields [`DecodeEvent::Reconfigured`] once the
579    /// headers are parsed.
580    ///
581    /// # Errors
582    ///
583    /// [`CodecError::PoolExhausted`] when the caller holds the frames in
584    /// every output buffer, so the device cannot decode further; drop frames
585    /// and decode the same access unit again. [`CodecError::Timeout`] when
586    /// the device makes no progress otherwise. [`CodecError::InvalidInput`]
587    /// when the access unit is empty or larger than the device's bitstream
588    /// buffers. Events that became ready before an error are returned by
589    /// the next call.
590    pub fn decode(&mut self, access_unit: &[u8], pts: u64) -> Result<Vec<DecodeEvent>> {
591        self.backend.decode(access_unit, pts)
592    }
593
594    /// Drains the decoder: returns every event not yet returned, in order.
595    ///
596    /// The decoder accepts new access units afterwards, starting a new
597    /// sequence: the next access unit must be decodable on its own, an IDR
598    /// picture with its SPS and PPS, as at the start of a stream or after a
599    /// seek.
600    ///
601    /// # Errors
602    ///
603    /// [`CodecError::PoolExhausted`] when the caller holds the frames in
604    /// every output buffer the drain needs; drop frames and flush again.
605    /// [`CodecError::Timeout`] when the device does not finish draining.
606    pub fn flush(&mut self) -> Result<Vec<DecodeEvent>> {
607        self.backend.flush()
608    }
609
610    /// The current stream's geometry, once known.
611    pub fn stream_info(&self) -> Option<&StreamInfo> {
612        self.backend.stream_info()
613    }
614
615    /// The device that decodes, for logs (a device node on Linux).
616    pub fn device(&self) -> String {
617        self.backend.device()
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    fn nv12_1080p() -> EncoderConfig {
626        EncoderConfig::h264(1920, 1080, PixelFormat::Nv12, 30.0)
627    }
628
629    #[test]
630    fn default_h264_config_is_valid() {
631        let c = nv12_1080p();
632        assert!(c.validate().is_ok());
633        assert_eq!(c.codec, VideoCodec::H264);
634        assert_eq!(c.bitrate, Bitrate::Auto);
635        assert!(c.repeat_headers);
636        assert!(c.gop.is_none() && c.profile.is_none() && c.level.is_none());
637    }
638
639    #[test]
640    fn rejects_empty_and_odd_sizes() {
641        let mut c = nv12_1080p();
642        c.width = 0;
643        assert!(matches!(c.validate(), Err(CodecError::InvalidConfig(_))));
644        let mut c = nv12_1080p();
645        c.height = 1081;
646        assert!(matches!(c.validate(), Err(CodecError::InvalidConfig(_))));
647        let mut c = EncoderConfig::h264(641, 480, PixelFormat::Yuyv, 30.0);
648        assert!(matches!(c.validate(), Err(CodecError::InvalidConfig(_))));
649        c.width = 640;
650        c.height = 481;
651        assert!(c.validate().is_ok(), "YUYV subsamples horizontally only");
652    }
653
654    #[test]
655    fn rejects_bad_rate_bitrate_and_gop() {
656        for fps in [0.0, -1.0, f64::NAN, f64::INFINITY] {
657            let mut c = nv12_1080p();
658            c.frame_rate = fps;
659            assert!(c.validate().is_err(), "{fps}");
660        }
661        let mut c = nv12_1080p();
662        c.bitrate = Bitrate::Bps(0);
663        assert!(c.validate().is_err());
664        let mut c = nv12_1080p();
665        c.gop = Some(0);
666        assert!(c.validate().is_err());
667        c.gop = Some(1);
668        c.bitrate = Bitrate::Bps(4_000_000);
669        assert!(c.validate().is_ok());
670    }
671
672    #[test]
673    fn frames_can_be_shared_between_threads() {
674        fn shareable<T: Send + Sync>() {}
675        shareable::<DecodedFrame>();
676        shareable::<Arc<DecodedFrame>>();
677    }
678
679    #[test]
680    fn a_dropped_frame_returns_its_buffer_to_its_own_pool() {
681        let returns = Arc::new(Returns::default());
682        let tensor =
683            |n| Arc::new(TensorDyn::new(&[n], edgefirst_tensor::DType::U8, None, None).unwrap());
684        let old = DecodedFrame::new(tensor(16), 7, false, (1, 3), Arc::clone(&returns));
685        let current = DecodedFrame::new(tensor(16), 8, true, (2, 5), Arc::clone(&returns));
686        assert_eq!((current.pts(), current.corrupted()), (8, true));
687        assert!(
688            returns.take(2).is_empty(),
689            "nothing returned while frames are held"
690        );
691        drop(old);
692        drop(current);
693        assert_eq!(
694            returns.take(2),
695            vec![5],
696            "a buffer of an earlier pool is not returned"
697        );
698        assert!(returns.take(1).is_empty(), "take drains every generation");
699    }
700
701    #[test]
702    fn levels_are_ordered() {
703        assert!(H264Level::L1b < H264Level::L1_1);
704        assert!(H264Level::L4_0 < H264Level::L5_1);
705    }
706}