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}