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}