Skip to main content

pixelflux/encoders/
v4l2m2m.rs

1//! Hardware H.264 through a generic stateful V4L2 M2M encoder.
2//!
3//! Boards that carry an encoder behind the kernel's own M2M interface publish no render node the
4//! VA-API backend can probe and no vendor library the NVENC one can load, so the ladder reaches
5//! software on hardware that encodes. A Raspberry Pi 4 is the case at hand (`bcm2835-codec`), and
6//! RK356x and i.MX8M carry the same interface, so the node is discovered by what it offers rather
7//! than by the board it sits on.
8//!
9//! One host frame becomes one access unit: the frame is copied into the output buffer, queued, and
10//! the access unit comes back on the capture queue. The copy is the floor here as it is on Tegra —
11//! X11 capture hands over host memory — and it costs 3.81 ms for a 1080p frame on a Pi 4, which is
12//! the same speed as a copy between two ordinary allocations: the `videobuf2-dma-contig` buffer
13//! takes a write stream at full memory bandwidth.
14//!
15//! Three things about such a device were measured rather than assumed, and each shapes the code:
16//!
17//! 1. The output queue takes packed 32-bit color and the firmware converts it, so nothing converts
18//!    on the CPU. The fourcc names are the opposite way round from the memory a caller holds:
19//!    `AB24` takes the BGRA an X11 capture produces, and `BGR4` takes RGBA. Decoding an encoded
20//!    chart is what settled that; reading the names would have swapped red and blue.
21//! 2. The conversion is BT.601 at full range, and it cannot be steered: a `S_FMT` asking for
22//!    REC709 at limited range comes back echoed and changes nothing. The stream also carries no
23//!    `video_signal_type`, so what the session declares has to be written into the SPS on the way
24//!    out, which `collect` is the place for.
25//! 3. Constant bitrate is not available, and asking for it does not merely fail: the driver hands
26//!    the firmware a rate control mode it refuses, and the next `STREAMON` answers `ESRCH`. A
27//!    control this session cannot set is therefore a refusal, never a warning — the node has to be
28//!    closed and reopened to be usable again.
29
30use std::mem::size_of;
31use std::os::fd::RawFd;
32use std::sync::OnceLock;
33
34use super::codec::{
35    Codec, VIDEO_HEADER_LEN, frame_type_from_key, h264_frame_type, h265_frame_type,
36    push_video_header, vp8_is_key, vp9_is_key,
37};
38use super::reference::Reference;
39use super::sps::{self, ColorSignal};
40use crate::RustCaptureSettings;
41
42const V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE: u32 = 9;
43const V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE: u32 = 10;
44const V4L2_MEMORY_MMAP: u32 = 1;
45const V4L2_FIELD_NONE: u32 = 1;
46const V4L2_CAP_VIDEO_M2M_MPLANE: u32 = 0x0000_8000;
47const V4L2_CAP_VIDEO_M2M: u32 = 0x0000_4000;
48
49/// Packed 32-bit color. The kernel spells these by channel order in a word and a caller holds
50/// bytes, so `AB24` is the BGRA of an X11 capture and `BGR4` is RGBA.
51const V4L2_PIX_FMT_BGRA: u32 = 0x3432_4241;
52const V4L2_PIX_FMT_RGBA: u32 = 0x3452_4742;
53const V4L2_PIX_FMT_H264: u32 = 0x3436_3248;
54const V4L2_PIX_FMT_HEVC: u32 = 0x4356_4548;
55const V4L2_PIX_FMT_VP8: u32 = 0x3038_5056;
56const V4L2_PIX_FMT_VP9: u32 = 0x3039_5056;
57
58/// The capture-queue format for a codec the kernel's interface names, or `None` for one it does
59/// not. The queues, the controls, and the input format are the same whichever of these a node
60/// carries, so the codec is a parameter rather than a second backend.
61pub fn coded_fourcc(codec: Codec) -> Option<u32> {
62    match codec {
63        Codec::H264 => Some(V4L2_PIX_FMT_H264),
64        Codec::H265 => Some(V4L2_PIX_FMT_HEVC),
65        Codec::Vp8 => Some(V4L2_PIX_FMT_VP8),
66        Codec::Vp9 => Some(V4L2_PIX_FMT_VP9),
67        Codec::Av1 | Codec::Jpeg => None,
68    }
69}
70
71const VIDIOC_QUERYCAP: u64 = 0x8068_5600;
72const VIDIOC_ENUM_FMT: u64 = 0xc040_5602;
73const VIDIOC_S_FMT: u64 = 0xc0d0_5605;
74const VIDIOC_REQBUFS: u64 = 0xc014_5608;
75const VIDIOC_QUERYBUF: u64 = 0xc058_5609;
76const VIDIOC_QBUF: u64 = 0xc058_560f;
77const VIDIOC_DQBUF: u64 = 0xc058_5611;
78const VIDIOC_STREAMON: u64 = 0x4004_5612;
79const VIDIOC_STREAMOFF: u64 = 0x4004_5613;
80const VIDIOC_S_PARM: u64 = 0xc0cc_5616;
81const VIDIOC_S_CTRL: u64 = 0xc008_561c;
82const VIDIOC_ENUM_FRAMESIZES: u64 = 0xc02c_564a;
83
84const CID_GOP_SIZE: u32 = 0x0099_09cb;
85const CID_BITRATE_MODE: u32 = 0x0099_09ce;
86const CID_BITRATE: u32 = 0x0099_09cf;
87const CID_REPEAT_SEQ_HEADER: u32 = 0x0099_09e2;
88const CID_FORCE_KEY_FRAME: u32 = 0x0099_09e5;
89const CID_H264_I_PERIOD: u32 = 0x0099_0a66;
90
91/// Variable is the only rate control such a device offers; see the note at the top about what
92/// asking for constant does to the session.
93const BITRATE_MODE_VBR: i32 = 0;
94
95const FRMSIZE_TYPE_DISCRETE: u32 = 1;
96const OUTPUT_BUFFERS: u32 = 1;
97const CAPTURE_BUFFERS: usize = 2;
98/// Long enough for a frame the hardware is slow on, short enough that a stalled node rebuilds the
99/// session rather than holding the stream. A 1080p frame takes about 35 ms on a Pi 4.
100const FRAME_TIMEOUT_MS: i32 = 500;
101
102#[repr(C)]
103#[derive(Clone, Copy, Default)]
104struct PlaneFormat {
105    sizeimage: u32,
106    bytesperline: u32,
107    reserved: [u16; 6],
108}
109
110#[repr(C)]
111struct PixFormatMplane {
112    width: u32,
113    height: u32,
114    pixelformat: u32,
115    field: u32,
116    colorspace: u32,
117    plane_fmt: [PlaneFormat; 8],
118    num_planes: u8,
119    flags: u8,
120    enc: u8,
121    quantization: u8,
122    xfer_func: u8,
123    reserved: [u8; 7],
124}
125
126#[repr(C)]
127struct Format {
128    type_: u32,
129    _pad: u32,
130    pix_mp: PixFormatMplane,
131    _tail: [u8; 8],
132}
133
134#[repr(C)]
135#[derive(Default)]
136struct RequestBuffers {
137    count: u32,
138    type_: u32,
139    memory: u32,
140    capabilities: u32,
141    flags: u8,
142    reserved: [u8; 3],
143}
144
145#[repr(C)]
146#[derive(Clone, Copy, Default)]
147struct Plane {
148    bytesused: u32,
149    length: u32,
150    m: u64,
151    data_offset: u32,
152    reserved: [u32; 11],
153}
154
155#[repr(C)]
156#[derive(Clone, Copy, Default)]
157struct Buffer {
158    index: u32,
159    type_: u32,
160    bytesused: u32,
161    flags: u32,
162    field: u32,
163    _pad: u32,
164    timestamp: [i64; 2],
165    timecode: [u32; 4],
166    sequence: u32,
167    memory: u32,
168    m: u64,
169    length: u32,
170    reserved2: u32,
171    request_fd: i32,
172}
173
174/// The union of `v4l2_streamparm` begins one word in, not two: the frame rate lands in the wrong
175/// place if this is laid out like `v4l2_format`.
176#[repr(C)]
177struct StreamParm {
178    type_: u32,
179    capability: u32,
180    outputmode: u32,
181    numerator: u32,
182    denominator: u32,
183    extendedmode: u32,
184    buffers: u32,
185    reserved: [u32; 4],
186    _tail: [u8; 160],
187}
188
189impl Default for StreamParm {
190    fn default() -> Self {
191        Self {
192            type_: 0,
193            capability: 0,
194            outputmode: 0,
195            numerator: 0,
196            denominator: 0,
197            extendedmode: 0,
198            buffers: 0,
199            reserved: [0; 4],
200            _tail: [0; 160],
201        }
202    }
203}
204
205#[repr(C)]
206struct Control {
207    id: u32,
208    value: i32,
209}
210
211#[repr(C)]
212struct Capability {
213    driver: [u8; 16],
214    card: [u8; 32],
215    bus_info: [u8; 32],
216    version: u32,
217    capabilities: u32,
218    device_caps: u32,
219    reserved: [u32; 3],
220}
221
222#[repr(C)]
223struct FmtDesc {
224    index: u32,
225    type_: u32,
226    flags: u32,
227    description: [u8; 32],
228    pixelformat: u32,
229    mbus_code: u32,
230    reserved: [u32; 3],
231}
232
233#[repr(C)]
234struct FrameSizeEnum {
235    index: u32,
236    pixel_format: u32,
237    type_: u32,
238    bounds: [u32; 6],
239    reserved: [u32; 2],
240}
241
242/// The sizes the ioctl numbers above encode, checked once so a layout that drifted is a refusal
243/// with a name rather than a scribble through a structure.
244fn abi_matches() -> Result<(), String> {
245    let sizes = [
246        ("v4l2_capability", size_of::<Capability>(), 104),
247        ("v4l2_fmtdesc", size_of::<FmtDesc>(), 64),
248        ("v4l2_format", size_of::<Format>(), 208),
249        ("v4l2_requestbuffers", size_of::<RequestBuffers>(), 20),
250        ("v4l2_plane", size_of::<Plane>(), 64),
251        ("v4l2_buffer", size_of::<Buffer>(), 88),
252        ("v4l2_streamparm", size_of::<StreamParm>(), 204),
253        ("v4l2_control", size_of::<Control>(), 8),
254        ("v4l2_frmsizeenum", size_of::<FrameSizeEnum>(), 44),
255    ];
256    for (name, got, want) in sizes {
257        if got != want {
258            return Err(format!(
259                "{name} is {got} bytes here, the interface wants {want}"
260            ));
261        }
262    }
263    Ok(())
264}
265
266unsafe fn ioctl<T>(fd: RawFd, request: u64, arg: *mut T) -> std::io::Result<()> {
267    // The request takes `as _` rather than a named type: `libc::Ioctl` is `c_ulong` on
268    // glibc and `c_int` on musl, so naming either one breaks the other's build.
269    let rc = unsafe { libc::ioctl(fd, request as _, arg as *mut libc::c_void) };
270    if rc < 0 {
271        return Err(std::io::Error::last_os_error());
272    }
273    Ok(())
274}
275
276fn empty_format(type_: u32) -> Format {
277    Format {
278        type_,
279        _pad: 0,
280        pix_mp: PixFormatMplane {
281            width: 0,
282            height: 0,
283            pixelformat: 0,
284            field: V4L2_FIELD_NONE,
285            colorspace: 0,
286            plane_fmt: [PlaneFormat::default(); 8],
287            num_planes: 1,
288            flags: 0,
289            enc: 0,
290            quantization: 0,
291            xfer_func: 0,
292            reserved: [0; 7],
293        },
294        _tail: [0; 8],
295    }
296}
297
298/// The fourcc for the bytes a caller holds. The kernel spells these by channel order in a
299/// word, so the name of each is the reverse of the memory it takes, and a session that picks
300/// by name rather than by byte order swaps red and blue without failing.
301fn input_format(rgba: bool) -> u32 {
302    if rgba {
303        V4L2_PIX_FMT_RGBA
304    } else {
305        V4L2_PIX_FMT_BGRA
306    }
307}
308
309/// What an encode node offers, read once for the process.
310struct NodeInfo {
311    path: String,
312    driver: String,
313    /// The coded formats its capture queue enumerates, which is what it encodes.
314    coded: Vec<u32>,
315    min_width: u32,
316    max_width: u32,
317    step_width: u32,
318    min_height: u32,
319    max_height: u32,
320    step_height: u32,
321}
322
323impl NodeInfo {
324    /// Whether this node encodes `codec`.
325    fn serves(&self, codec: Codec) -> bool {
326        coded_fourcc(codec).is_some_and(|f| self.coded.contains(&f))
327    }
328
329    /// Whether a session of this size can come up, answered before anything is built: a refusal
330    /// here falls through to software, a refusal later leaves a black stream.
331    fn fits(&self, width: u32, height: u32) -> bool {
332        let in_range = |value: u32, min: u32, max: u32, step: u32| {
333            value >= min && value <= max && (step == 0 || (value - min).is_multiple_of(step))
334        };
335        in_range(width, self.min_width, self.max_width, self.step_width)
336            && in_range(height, self.min_height, self.max_height, self.step_height)
337    }
338}
339
340fn enumerates(fd: RawFd, type_: u32, wanted: u32) -> bool {
341    for index in 0..64u32 {
342        let mut desc = FmtDesc {
343            index,
344            type_,
345            flags: 0,
346            description: [0; 32],
347            pixelformat: 0,
348            mbus_code: 0,
349            reserved: [0; 3],
350        };
351        if unsafe { ioctl(fd, VIDIOC_ENUM_FMT, &mut desc) }.is_err() {
352            return false;
353        }
354        if desc.pixelformat == wanted {
355            return true;
356        }
357    }
358    false
359}
360
361fn frame_sizes(fd: RawFd, path: &str, coded: u32) -> Option<NodeInfo> {
362    let mut sizes = FrameSizeEnum {
363        index: 0,
364        pixel_format: coded,
365        type_: 0,
366        bounds: [0; 6],
367        reserved: [0; 2],
368    };
369    unsafe { ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &mut sizes) }.ok()?;
370    let (min_w, max_w, step_w, min_h, max_h, step_h) = if sizes.type_ == FRMSIZE_TYPE_DISCRETE {
371        let (w, h) = (sizes.bounds[0], sizes.bounds[1]);
372        (w, w, 0, h, h, 0)
373    } else {
374        (
375            sizes.bounds[0],
376            sizes.bounds[1],
377            sizes.bounds[2],
378            sizes.bounds[3],
379            sizes.bounds[4],
380            sizes.bounds[5],
381        )
382    };
383    Some(NodeInfo {
384        path: path.to_string(),
385        driver: String::new(),
386        coded: Vec::new(),
387        min_width: min_w,
388        max_width: max_w,
389        step_width: step_w,
390        min_height: min_h,
391        max_height: max_h,
392        step_height: step_h,
393    })
394}
395
396/// The first node that encodes H.264 from packed color, found by asking rather than by name: the
397/// number a board gives its encoder is not a contract, and the same interface appears on boards
398/// with entirely different numbering.
399fn node() -> Option<&'static NodeInfo> {
400    static NODE: OnceLock<Option<NodeInfo>> = OnceLock::new();
401    NODE.get_or_init(|| {
402        if let Err(e) = abi_matches() {
403            crate::log::debug!("[pixelflux] V4L2 M2M unusable: {e}");
404            return None;
405        }
406        for index in 0..64 {
407            let path = format!("/dev/video{index}");
408            let Ok(c_path) = std::ffi::CString::new(path.clone()) else {
409                continue;
410            };
411            let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR) };
412            if fd < 0 {
413                continue;
414            }
415            let found = (|| {
416                let mut caps: Capability = unsafe { std::mem::zeroed() };
417                unsafe { ioctl(fd, VIDIOC_QUERYCAP, &mut caps) }.ok()?;
418                if caps.device_caps & (V4L2_CAP_VIDEO_M2M_MPLANE | V4L2_CAP_VIDEO_M2M) == 0 {
419                    return None;
420                }
421                let coded: Vec<u32> = [Codec::H264, Codec::H265, Codec::Vp8, Codec::Vp9]
422                    .into_iter()
423                    .filter_map(coded_fourcc)
424                    .filter(|&f| enumerates(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, f))
425                    .collect();
426                if coded.is_empty() {
427                    return None;
428                }
429                if !enumerates(fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, V4L2_PIX_FMT_BGRA) {
430                    return None;
431                }
432                let driver = String::from_utf8_lossy(&caps.driver)
433                    .trim_end_matches('\0')
434                    .to_string();
435                frame_sizes(fd, &path, coded[0]).map(|info| NodeInfo {
436                    driver,
437                    coded,
438                    ..info
439                })
440            })();
441            unsafe { libc::close(fd) };
442            if let Some(info) = found {
443                println!(
444                    "[pixelflux] V4L2 M2M encoder on {} up to {}x{}.",
445                    info.path, info.max_width, info.max_height
446                );
447                return Some(info);
448            }
449        }
450        None
451    })
452    .as_ref()
453}
454
455/// The rates to try, in order: what the session asked for, then the ones a device that refuses
456/// it is likely to take. A device is asked at its own rate first, so one that can do it is never
457/// slowed, and the steps below are the rates video hardware is built around rather than a search.
458fn fallback_rates(wanted: f64) -> Vec<f64> {
459    let mut rates = vec![wanted];
460    for step in [30.0, 15.0] {
461        if step < wanted - 0.01 {
462            rates.push(step);
463        }
464    }
465    rates
466}
467
468/// What a device is known to convert RGB with when it declares nothing itself. Only a device
469/// whose behavior is on record earns an entry: a Raspberry Pi with firmware older than August
470/// 2024 converts to full range BT.601 and writes no `video_signal_type`, which the driver's
471/// author states in the firmware tracker and a decoded chart confirms. Guessing for a device not
472/// on this list would put a claim in the stream that nobody has checked, and a wrong declaration
473/// colors a picture worse than an absent one.
474fn known_conversion(driver: &str) -> Option<ColorSignal> {
475    match driver {
476        "bcm2835-codec" => Some(ColorSignal::BT601_FULL),
477        _ => None,
478    }
479}
480
481/// What the session has learned about the color its device produces.
482enum Color {
483    /// No access unit has come back yet, and nothing is on record for this device either, so
484    /// what the stream will say is not known until one arrives.
485    Unknown,
486    /// The device declares its own color, whatever it is, and the stream is left alone.
487    Declared(ColorSignal),
488    /// The device declares nothing and its conversion is on record, so every sequence parameter
489    /// set is written with it on the way out.
490    Writing(ColorSignal),
491    /// The device declares nothing and nothing is known about it, so nothing is claimed.
492    Untagged,
493}
494
495/// Whether this machine carries an M2M encode node at all.
496pub fn available() -> bool {
497    node().is_some()
498}
499
500/// Whether a session of this codec and size would come up on it, so the ladder can fall through
501/// to software before a session exists rather than after one has failed.
502pub fn encodes(codec: Codec, width: i32, height: i32) -> bool {
503    match node() {
504        Some(info) if width > 0 && height > 0 => {
505            info.serves(codec) && info.fits(width as u32, height as u32)
506        }
507        _ => false,
508    }
509}
510
511/// The codecs the node encodes, for the report a caller reads before any session exists.
512pub fn served() -> Vec<Codec> {
513    match node() {
514        Some(info) => [Codec::H264, Codec::H265, Codec::Vp8, Codec::Vp9]
515            .into_iter()
516            .filter(|&c| info.serves(c))
517            .collect(),
518        None => Vec::new(),
519    }
520}
521
522pub struct V4l2M2mEncoder {
523    fd: RawFd,
524    codec: Codec,
525    coded: u32,
526    width: i32,
527    height: i32,
528    row_bytes: usize,
529    bytesperline: usize,
530    output_size: usize,
531    output_map: *mut libc::c_void,
532    capture: [(*mut libc::c_void, usize); CAPTURE_BUFFERS],
533    bitrate_bps: u32,
534    fps: f64,
535    omit_headers: bool,
536    color: Color,
537    /// What to declare when the device declares nothing, or `None` for a device off the record.
538    fallback: Option<ColorSignal>,
539    /// A sequence parameter set arrives with every key frame and is identical each time, so it is
540    /// written once and matched by bytes after that.
541    written_sps: Option<(Vec<u8>, Vec<u8>)>,
542    /// Whether the first access unit has settled what the stream says.
543    decided: bool,
544    /// The reorder bound every H.264 set is held to. The session queues one frame and waits for
545    /// its unit before the next, so pictures leave in the order they are shown whatever the
546    /// device's picture order count: a device that reordered would wait for a frame never queued.
547    reorder: sps::NoReorder,
548}
549
550unsafe impl Send for V4l2M2mEncoder {}
551
552impl V4l2M2mEncoder {
553    pub fn new(codec: Codec, settings: &RustCaptureSettings, rgba: bool) -> Result<Self, String> {
554        let info = node().ok_or("no V4L2 M2M encode node")?;
555        let coded = coded_fourcc(codec)
556            .filter(|f| info.coded.contains(f))
557            .ok_or_else(|| format!("{} encodes no {}", info.path, codec.display()))?;
558        let (width, height) = (settings.width, settings.height);
559        if width <= 0 || height <= 0 {
560            return Err(format!(
561                "the encoder needs positive dimensions, got {width}x{height}"
562            ));
563        }
564        if !info.fits(width as u32, height as u32) {
565            return Err(format!(
566                "{} encodes {}x{} to {}x{}, not {width}x{height}",
567                info.path, info.min_width, info.min_height, info.max_width, info.max_height
568            ));
569        }
570        let path = std::ffi::CString::new(info.path.clone()).map_err(|e| e.to_string())?;
571        let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
572        if fd < 0 {
573            return Err(format!(
574                "{} did not open: {}",
575                info.path,
576                std::io::Error::last_os_error()
577            ));
578        }
579        let mut encoder = Self {
580            fd,
581            codec,
582            coded,
583            width,
584            height,
585            row_bytes: width as usize * 4,
586            bytesperline: 0,
587            output_size: 0,
588            output_map: libc::MAP_FAILED,
589            capture: [(libc::MAP_FAILED, 0); CAPTURE_BUFFERS],
590            bitrate_bps: (settings.video_bitrate_kbps.max(1) as u32).saturating_mul(1000),
591            fps: settings.target_fps.max(1.0),
592            omit_headers: settings.omit_stripe_headers,
593            // A device on record is described from the start rather than from its first access
594            // unit: the session line is printed before a frame exists, and a line saying limited
595            // range while the stream will say full is a line that misleads whoever reads it.
596            color: match known_conversion(&info.driver) {
597                Some(signal) => Color::Writing(signal),
598                None => Color::Unknown,
599            },
600            fallback: known_conversion(&info.driver),
601            written_sps: None,
602            decided: false,
603            reorder: sps::NoReorder::new("The M2M encoder", true),
604        };
605        // A device answers "not at that rate" only by refusing, and only once the port is
606        // enabled: the frame rate reaches the firmware at `STREAMON`, not at the ioctl that
607        // carried it, and `VIDIOC_ENUM_FRAMEINTERVALS` is not implemented to ask beforehand. A
608        // Raspberry Pi 4 takes 1080p at 30 and refuses it at 60, which is the rate a session
609        // asks for by default, so a single attempt would leave a capable device unused.
610        let mut last = String::new();
611        for rate in fallback_rates(settings.target_fps.max(1.0)) {
612            encoder.fps = rate;
613            match encoder.setup(settings, rgba) {
614                Ok(()) => {
615                    if rate < settings.target_fps.max(1.0) {
616                        println!(
617                            "[pixelflux] The M2M encoder refused {:.0} frames a second and took {:.0}.",
618                            settings.target_fps.max(1.0),
619                            rate
620                        );
621                    }
622                    return Ok(encoder);
623                }
624                Err(e) => {
625                    last = e;
626                    encoder.release();
627                    let reopened = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
628                    if reopened < 0 {
629                        return Err(last);
630                    }
631                    encoder.fd = reopened;
632                }
633            }
634        }
635        Err(last)
636    }
637
638    /// Everything about the session that a refused `STREAMON` leaves behind, so the next attempt
639    /// starts on a node that carries none of it. The descriptor itself is reopened by the caller:
640    /// a device that refused a parameter answers `ESRCH` to every later call on that descriptor.
641    fn release(&mut self) {
642        let _ = self.stream(
643            VIDIOC_STREAMOFF,
644            V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
645            "STREAMOFF output",
646        );
647        let _ = self.stream(
648            VIDIOC_STREAMOFF,
649            V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
650            "STREAMOFF capture",
651        );
652        if self.output_map != libc::MAP_FAILED {
653            unsafe { libc::munmap(self.output_map, self.output_size) };
654            self.output_map = libc::MAP_FAILED;
655        }
656        for slot in 0..CAPTURE_BUFFERS {
657            let (map, length) = self.capture[slot];
658            if map != libc::MAP_FAILED {
659                unsafe { libc::munmap(map, length) };
660                self.capture[slot] = (libc::MAP_FAILED, 0);
661            }
662        }
663        if self.fd >= 0 {
664            unsafe { libc::close(self.fd) };
665            self.fd = -1;
666        }
667    }
668
669    fn setup(&mut self, settings: &RustCaptureSettings, rgba: bool) -> Result<(), String> {
670        let input = input_format(rgba);
671        let mut output_format = empty_format(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE);
672        output_format.pix_mp.width = self.width as u32;
673        output_format.pix_mp.height = self.height as u32;
674        output_format.pix_mp.pixelformat = input;
675        output_format.pix_mp.plane_fmt[0].bytesperline = self.row_bytes as u32;
676        output_format.pix_mp.plane_fmt[0].sizeimage =
677            (self.row_bytes * self.height as usize) as u32;
678        self.call(VIDIOC_S_FMT, &mut output_format, "S_FMT output")?;
679        self.bytesperline = output_format.pix_mp.plane_fmt[0].bytesperline as usize;
680        self.output_size = output_format.pix_mp.plane_fmt[0].sizeimage as usize;
681        if self.bytesperline < self.row_bytes
682            || self.output_size < self.bytesperline * self.height as usize
683        {
684            return Err(format!(
685                "the node offered {} bytes per line and {} per frame for {}x{}",
686                self.bytesperline, self.output_size, self.width, self.height
687            ));
688        }
689
690        let mut capture_format = empty_format(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
691        capture_format.pix_mp.width = self.width as u32;
692        capture_format.pix_mp.height = self.height as u32;
693        capture_format.pix_mp.pixelformat = self.coded;
694        capture_format.pix_mp.plane_fmt[0].sizeimage =
695            ((self.width * self.height) as u32).max(2 << 20);
696        self.call(VIDIOC_S_FMT, &mut capture_format, "S_FMT capture")?;
697
698        self.set_frame_rate(self.fps)?;
699        let seconds = if settings.keyframe_interval_s > 0.0 {
700            settings.keyframe_interval_s
701        } else {
702            10.0
703        };
704        let keyframe = ((self.fps * seconds) as i32).clamp(1, 600);
705        self.set_control(CID_BITRATE_MODE, BITRATE_MODE_VBR, "rate control mode")?;
706        self.set_control(CID_BITRATE, self.bitrate_bps as i32, "bitrate")?;
707        self.set_control(
708            CID_REPEAT_SEQ_HEADER,
709            1,
710            "sequence header with every key frame",
711        )?;
712        self.set_control(CID_H264_I_PERIOD, keyframe, "key frame period")?;
713        self.set_control(CID_GOP_SIZE, keyframe, "GOP size")?;
714
715        self.request_buffers(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, OUTPUT_BUFFERS)?;
716        self.request_buffers(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, CAPTURE_BUFFERS as u32)?;
717        self.output_map = self.map_buffer(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, 0)?.0;
718        for index in 0..CAPTURE_BUFFERS {
719            self.capture[index] =
720                self.map_buffer(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index as u32)?;
721        }
722
723        self.stream(
724            VIDIOC_STREAMON,
725            V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
726            "STREAMON capture",
727        )?;
728        self.stream(
729            VIDIOC_STREAMON,
730            V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
731            "STREAMON output",
732        )?;
733        for index in 0..CAPTURE_BUFFERS {
734            self.queue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index as u32, 0)?;
735        }
736        Ok(())
737    }
738
739    fn call<T>(&self, request: u64, arg: &mut T, what: &str) -> Result<(), String> {
740        unsafe { ioctl(self.fd, request, arg) }.map_err(|e| format!("{what} failed: {e}"))
741    }
742
743    /// A control this device refuses is a refusal of the session. It is not a warning to carry on
744    /// past: a rejected rate control mode leaves the node answering `ESRCH` to `STREAMON` until it
745    /// is closed, so a session that shrugged here would fail later and in a place that says
746    /// nothing about the cause.
747    fn set_control(&self, id: u32, value: i32, what: &str) -> Result<(), String> {
748        let mut control = Control { id, value };
749        self.call(VIDIOC_S_CTRL, &mut control, &format!("setting {what}"))
750    }
751
752    fn set_frame_rate(&mut self, fps: f64) -> Result<(), String> {
753        let mut parm = StreamParm {
754            type_: V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
755            numerator: 1000,
756            denominator: (fps * 1000.0).round().max(1000.0) as u32,
757            ..Default::default()
758        };
759        self.call(VIDIOC_S_PARM, &mut parm, "S_PARM frame rate")?;
760        self.fps = fps;
761        Ok(())
762    }
763
764    fn request_buffers(&self, type_: u32, count: u32) -> Result<(), String> {
765        let mut request = RequestBuffers {
766            count,
767            type_,
768            memory: V4L2_MEMORY_MMAP,
769            ..Default::default()
770        };
771        self.call(VIDIOC_REQBUFS, &mut request, "REQBUFS")?;
772        if request.count < count {
773            return Err(format!(
774                "the node gave {} buffers of the {count} asked for",
775                request.count
776            ));
777        }
778        Ok(())
779    }
780
781    fn map_buffer(&self, type_: u32, index: u32) -> Result<(*mut libc::c_void, usize), String> {
782        let mut planes = [Plane::default(); 1];
783        let mut buffer = Buffer {
784            index,
785            type_,
786            memory: V4L2_MEMORY_MMAP,
787            m: planes.as_mut_ptr() as u64,
788            length: 1,
789            ..Default::default()
790        };
791        self.call(VIDIOC_QUERYBUF, &mut buffer, "QUERYBUF")?;
792        let length = planes[0].length as usize;
793        let map = unsafe {
794            libc::mmap(
795                std::ptr::null_mut(),
796                length,
797                libc::PROT_READ | libc::PROT_WRITE,
798                libc::MAP_SHARED,
799                self.fd,
800                planes[0].m as libc::off_t,
801            )
802        };
803        if map == libc::MAP_FAILED {
804            return Err(format!(
805                "mapping a buffer failed: {}",
806                std::io::Error::last_os_error()
807            ));
808        }
809        Ok((map, length))
810    }
811
812    fn queue(&self, type_: u32, index: u32, bytesused: u32) -> Result<(), String> {
813        let mut planes = [Plane::default(); 1];
814        planes[0].bytesused = bytesused;
815        let mut buffer = Buffer {
816            index,
817            type_,
818            memory: V4L2_MEMORY_MMAP,
819            m: planes.as_mut_ptr() as u64,
820            length: 1,
821            ..Default::default()
822        };
823        self.call(VIDIOC_QBUF, &mut buffer, "QBUF")
824    }
825
826    fn dequeue(&self, type_: u32) -> Result<(u32, usize), String> {
827        let mut planes = [Plane::default(); 1];
828        let mut buffer = Buffer {
829            type_,
830            memory: V4L2_MEMORY_MMAP,
831            m: planes.as_mut_ptr() as u64,
832            length: 1,
833            ..Default::default()
834        };
835        self.call(VIDIOC_DQBUF, &mut buffer, "DQBUF")?;
836        Ok((buffer.index, planes[0].bytesused as usize))
837    }
838
839    fn stream(&self, request: u64, type_: u32, what: &str) -> Result<(), String> {
840        let mut argument = type_ as i32;
841        self.call(request, &mut argument, what)
842    }
843
844    /// Wait for the access unit of the frame just queued. The node is opened blocking, so this
845    /// exists to bound the wait: a device that stopped answering has to become an error the ladder
846    /// can act on, not a stream that never returns.
847    fn wait_for_frame(&self) -> Result<(), String> {
848        let mut poll = libc::pollfd {
849            fd: self.fd,
850            events: libc::POLLIN,
851            revents: 0,
852        };
853        let ready = unsafe { libc::poll(&mut poll, 1, FRAME_TIMEOUT_MS) };
854        match ready {
855            1 => Ok(()),
856            0 => Err(format!(
857                "the encoder produced nothing in {FRAME_TIMEOUT_MS} ms"
858            )),
859            _ => Err(format!(
860                "waiting for a frame failed: {}",
861                std::io::Error::last_os_error()
862            )),
863        }
864    }
865
866    fn fill_output(&self, pixels: &[u8], stride: usize) -> Result<(), String> {
867        let height = self.height as usize;
868        let needed = stride * (height - 1) + self.row_bytes;
869        if stride < self.row_bytes || pixels.len() < needed {
870            return Err("input buffer too small".into());
871        }
872        let destination = self.output_map as *mut u8;
873        if stride == self.bytesperline {
874            unsafe {
875                std::ptr::copy_nonoverlapping(
876                    pixels.as_ptr(),
877                    destination,
878                    self.bytesperline * height,
879                )
880            };
881            return Ok(());
882        }
883        for row in 0..height {
884            unsafe {
885                std::ptr::copy_nonoverlapping(
886                    pixels.as_ptr().add(row * stride),
887                    destination.add(row * self.bytesperline),
888                    self.row_bytes,
889                );
890            }
891        }
892        Ok(())
893    }
894
895    pub fn codec(&self) -> Codec {
896        self.codec
897    }
898
899    /// The wire picture type of one access unit, read with the codec's own syntax: an H.265 NAL
900    /// header is two bytes where H.264's is one, and VP8 and VP9 carry no NAL units at all.
901    fn frame_type(&self, bytes: &[u8]) -> u8 {
902        match self.codec {
903            Codec::H265 => h265_frame_type(bytes),
904            Codec::Vp8 => frame_type_from_key(vp8_is_key(bytes)),
905            Codec::Vp9 => frame_type_from_key(vp9_is_key(bytes)),
906            _ => h264_frame_type(bytes),
907        }
908    }
909
910    /// The firmware converts to 4:2:0 and the encoder takes nothing else.
911    pub fn is_fullcolor(&self) -> bool {
912        false
913    }
914
915    /// What the stream says about its color, once an access unit has settled it.
916    fn signal(&self) -> Option<ColorSignal> {
917        match self.color {
918            Color::Declared(signal) | Color::Writing(signal) => Some(signal),
919            Color::Unknown | Color::Untagged => None,
920        }
921    }
922
923    /// The kernel's stateful interface names no reference and takes no invalidation.
924    pub fn last_reference(&self) -> Reference {
925        Reference::Untracked
926    }
927
928    pub fn invalidate_reference(&mut self, _frame_id: u16) -> bool {
929        false
930    }
931
932    /// The range the stream declares, which is the device's to decide: a session that says
933    /// nothing is described as limited, since that is what a decoder given nothing assumes.
934    pub fn is_full_range(&self) -> bool {
935        self.signal().is_some_and(|signal| signal.full_range)
936    }
937
938    /// Learn what the first sequence parameter set says, and write one into every later set when
939    /// the device said nothing and its conversion is on record; then hold every set to a reorder
940    /// bound of zero. Only H.264 carries either here: an H.265 set names its own reordering, and
941    /// VP8 and VP9 carry no parameter sets.
942    fn tag_sequence(&mut self, unit: &[u8]) -> Option<Vec<u8>> {
943        if self.codec != Codec::H264 {
944            return None;
945        }
946        let sets = sps::sequence_parameter_sets(unit);
947        if sets.is_empty() {
948            return None;
949        }
950        if !self.decided {
951            self.decided = true;
952            let (start, end) = sets[0];
953            self.color = match sps::read_color(&unit[start..end]) {
954                Some(signal) => {
955                    println!(
956                        "[pixelflux] The M2M encoder declares its own color: matrix {}, {} range.",
957                        signal.matrix,
958                        if signal.full_range { "full" } else { "limited" }
959                    );
960                    Color::Declared(signal)
961                }
962                None => match self.fallback {
963                    Some(signal) => {
964                        println!(
965                            "[pixelflux] The M2M encoder declares no color; writing matrix {}, {} range into its headers.",
966                            signal.matrix,
967                            if signal.full_range { "full" } else { "limited" }
968                        );
969                        Color::Writing(signal)
970                    }
971                    None => {
972                        eprintln!(
973                            "[pixelflux] The M2M encoder declares no color and none is on record for it; the stream carries none, and a decoder will guess from the frame size."
974                        );
975                        Color::Untagged
976                    }
977                },
978            };
979        }
980
981        let mut out: Option<Vec<u8>> = None;
982        let mut copied = 0;
983        for (start, end) in sets {
984            let original = &unit[start..end];
985            let colored = match self.color {
986                Color::Writing(signal) => self.write_color(original, signal),
987                _ => None,
988            };
989            let set = colored.as_deref().unwrap_or(original);
990            let Some(replacement) = self.reorder.set(set).or(colored) else {
991                continue;
992            };
993            let out = out.get_or_insert_with(|| Vec::with_capacity(unit.len() + 16 * 4));
994            out.extend_from_slice(&unit[copied..start]);
995            out.extend_from_slice(&replacement);
996            copied = end;
997        }
998        let mut out = out?;
999        out.extend_from_slice(&unit[copied..]);
1000        Some(out)
1001    }
1002
1003    /// `original` declaring `signal`, or None where the set cannot be written, in which case it
1004    /// is left as it came: a stream that carries no color is watchable, and one carrying a set
1005    /// built wrong is not.
1006    fn write_color(&mut self, original: &[u8], signal: ColorSignal) -> Option<Vec<u8>> {
1007        if let Some((was, now)) = &self.written_sps
1008            && was == original
1009        {
1010            return Some(now.clone());
1011        }
1012        match sps::write_color(original, signal) {
1013            Ok(written) => {
1014                self.written_sps = Some((original.to_vec(), written.clone()));
1015                Some(written)
1016            }
1017            Err(e) => {
1018                if self.written_sps.is_none() {
1019                    eprintln!("[pixelflux] The sequence parameter set was left as it came: {e}");
1020                    self.color = Color::Untagged;
1021                }
1022                None
1023            }
1024        }
1025    }
1026
1027    /// Apply a live bitrate or frame rate change. Both are writable while streaming on this path,
1028    /// and the frame rate goes down with the bitrate: left behind, it would budget a new bitrate
1029    /// against an old rate.
1030    pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) -> Result<(), String> {
1031        let wanted = (settings.video_bitrate_kbps.max(1) as u32).saturating_mul(1000);
1032        let fps = settings.target_fps.max(1.0);
1033        if (fps - self.fps).abs() > 0.01 {
1034            self.set_frame_rate(fps)?;
1035        }
1036        if wanted != self.bitrate_bps {
1037            self.set_control(CID_BITRATE, wanted as i32, "bitrate")?;
1038            self.bitrate_bps = wanted;
1039        }
1040        Ok(())
1041    }
1042
1043    pub fn encode_host(
1044        &mut self,
1045        pixels: &[u8],
1046        stride: usize,
1047        _rgba: bool,
1048        frame_number: u64,
1049        _qp: u32,
1050        force_idr: bool,
1051    ) -> Result<Vec<u8>, String> {
1052        self.fill_output(pixels, stride)?;
1053        if force_idr {
1054            self.set_control(CID_FORCE_KEY_FRAME, 1, "force key frame")?;
1055        }
1056        self.queue(
1057            V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
1058            0,
1059            self.output_size as u32,
1060        )?;
1061        self.wait_for_frame()?;
1062        let (index, length) = self.dequeue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)?;
1063        let slot = index as usize;
1064        if slot >= CAPTURE_BUFFERS {
1065            return Err(format!("the node returned capture buffer {slot}"));
1066        }
1067        let (data, _) = self.capture[slot];
1068        let unit = unsafe { std::slice::from_raw_parts(data as *const u8, length) };
1069        let out = self.frame(unit, frame_number);
1070        self.queue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index, 0)?;
1071        self.dequeue(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)?;
1072        Ok(out)
1073    }
1074
1075    /// One access unit as the wire carries it: its sequence parameter sets tagged, behind a
1076    /// header naming the session's codec unless the caller frames the stream itself.
1077    fn frame(&mut self, unit: &[u8], frame_number: u64) -> Vec<u8> {
1078        let tagged = self.tag_sequence(unit);
1079        let bytes = tagged.as_deref().unwrap_or(unit);
1080        let mut out = Vec::with_capacity(VIDEO_HEADER_LEN + bytes.len());
1081        if !self.omit_headers {
1082            push_video_header(
1083                &mut out,
1084                self.codec,
1085                self.frame_type(bytes),
1086                frame_number as u16,
1087                0,
1088                self.width as u16,
1089                self.height as u16,
1090                Reference::Untracked,
1091            );
1092        }
1093        out.extend_from_slice(bytes);
1094        out
1095    }
1096}
1097
1098impl Drop for V4l2M2mEncoder {
1099    fn drop(&mut self) {
1100        let _ = self.stream(
1101            VIDIOC_STREAMOFF,
1102            V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
1103            "STREAMOFF output",
1104        );
1105        let _ = self.stream(
1106            VIDIOC_STREAMOFF,
1107            V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
1108            "STREAMOFF capture",
1109        );
1110        if self.output_map != libc::MAP_FAILED {
1111            unsafe { libc::munmap(self.output_map, self.output_size) };
1112        }
1113        for (map, length) in self.capture {
1114            if map != libc::MAP_FAILED {
1115                unsafe { libc::munmap(map, length) };
1116            }
1117        }
1118        if self.fd >= 0 {
1119            unsafe { libc::close(self.fd) };
1120        }
1121    }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126    use super::*;
1127    use crate::encoders::sps::fixtures::{PI4_SPS, VCE_SPS, reorder_of, without};
1128
1129    /// A session of `codec` on a device of `driver` that never opened a node, for what a session
1130    /// does with the units a device hands back.
1131    fn stand_in(codec: Codec, driver: &str) -> V4l2M2mEncoder {
1132        V4l2M2mEncoder {
1133            fd: -1,
1134            codec,
1135            coded: coded_fourcc(codec).unwrap_or(0),
1136            width: 1280,
1137            height: 720,
1138            row_bytes: 1280 * 4,
1139            bytesperline: 0,
1140            output_size: 0,
1141            output_map: libc::MAP_FAILED,
1142            capture: [(libc::MAP_FAILED, 0); CAPTURE_BUFFERS],
1143            bitrate_bps: 8_000_000,
1144            fps: 30.0,
1145            omit_headers: true,
1146            color: match known_conversion(driver) {
1147                Some(signal) => Color::Writing(signal),
1148                None => Color::Unknown,
1149            },
1150            fallback: known_conversion(driver),
1151            written_sps: None,
1152            decided: false,
1153            reorder: sps::NoReorder::new("The M2M encoder", true),
1154        }
1155    }
1156
1157    /// An access unit as a device hands it back: the set, a PPS, and an IDR slice.
1158    fn unit(sps: &[u8]) -> Vec<u8> {
1159        [
1160            &[0, 0, 0, 1][..],
1161            sps,
1162            &[0, 0, 0, 1, 0x68, 0xee, 0x3c, 0x80],
1163            &[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00, 0x21],
1164        ]
1165        .concat()
1166    }
1167
1168    /// The first set of an access unit.
1169    fn first_set(unit: &[u8]) -> Vec<u8> {
1170        let (start, end) = sps::sequence_parameter_sets(unit)[0];
1171        unit[start..end].to_vec()
1172    }
1173
1174    /// Every H.264 set a device hands back leaves bounded at zero: the session queues a frame
1175    /// and waits for its unit, so nothing it encodes can reorder whatever picture order count
1176    /// the device writes. A device that declares its own color keeps it.
1177    #[test]
1178    fn a_device_set_leaves_bounded_at_zero() {
1179        let mut session = stand_in(Codec::H264, "unknown-codec");
1180        let tagged = session
1181            .tag_sequence(&unit(VCE_SPS))
1182            .expect("a set to bound");
1183        assert_eq!(reorder_of(&first_set(&tagged)), Some((0, 2)));
1184        assert_eq!(
1185            sps::read_color(&first_set(&tagged)),
1186            sps::read_color(VCE_SPS),
1187            "the device's own color moved"
1188        );
1189        assert!(
1190            tagged.ends_with(&unit(VCE_SPS)[4 + VCE_SPS.len()..]),
1191            "the units after the set changed"
1192        );
1193        assert_eq!(
1194            session.tag_sequence(&unit(VCE_SPS)),
1195            Some(tagged),
1196            "the next key frame's set differs"
1197        );
1198        assert_eq!(
1199            session.tag_sequence(&[0, 0, 0, 1, 0x41, 0x9a, 0x02]),
1200            None,
1201            "a delta without a set was touched"
1202        );
1203    }
1204
1205    /// A Raspberry Pi 4 on an old firmware declares no color and bounds its own stream: the
1206    /// color on record is written and the bound it declared survives the write; without its own
1207    /// bound it is given one next to the color.
1208    #[test]
1209    fn the_pi_color_and_the_bound_are_written_together() {
1210        let mut session = stand_in(Codec::H264, "bcm2835-codec");
1211        let tagged = first_set(
1212            &session
1213                .tag_sequence(&unit(PI4_SPS))
1214                .expect("a color to write"),
1215        );
1216        assert_eq!(sps::read_color(&tagged), Some(ColorSignal::BT601_FULL));
1217        assert_eq!(reorder_of(&tagged), Some((0, 1)));
1218        let mut session = stand_in(Codec::H264, "bcm2835-codec");
1219        let bare = without(PI4_SPS, false);
1220        let tagged = first_set(&session.tag_sequence(&unit(&bare)).expect("a set to write"));
1221        assert_eq!(sps::read_color(&tagged), Some(ColorSignal::BT601_FULL));
1222        assert_eq!(reorder_of(&tagged), Some((0, 1)));
1223    }
1224
1225    /// The wire header names the codec the node encodes, which is what a client builds its
1226    /// decoder from: an H.265 or VP8 unit labeled H.264 is decoded as H.264.
1227    #[test]
1228    fn the_header_names_the_session_codec() {
1229        use crate::encoders::codec::{FRAME_KEY, parse_video_type};
1230        let units: [(Codec, Vec<u8>); 3] = [
1231            (Codec::H264, unit(VCE_SPS)),
1232            (
1233                Codec::H265,
1234                vec![
1235                    0, 0, 0, 1, 0x40, 0x01, 0x0c, 0, 0, 0, 1, 0x26, 0x01, 0xaf, 0x10,
1236                ],
1237            ),
1238            (
1239                Codec::Vp8,
1240                vec![0x50, 0x2a, 0x00, 0x9d, 0x01, 0x2a, 0x00, 0x05, 0xd0, 0x02],
1241            ),
1242        ];
1243        for (codec, bytes) in units {
1244            let mut session = stand_in(codec, "unknown-codec");
1245            session.omit_headers = false;
1246            let framed = session.frame(&bytes, 7);
1247            assert_eq!(
1248                parse_video_type(framed[1]),
1249                Some((codec, FRAME_KEY)),
1250                "{codec:?}"
1251            );
1252            assert_eq!(&framed[2..4], &[0, 7], "{codec:?}: the frame number");
1253        }
1254    }
1255
1256    /// Only H.264 carries these sets: bytes of another codec that happen to look like one are
1257    /// passed through untouched.
1258    #[test]
1259    fn other_codecs_are_passed_through() {
1260        for codec in [Codec::H265, Codec::Vp8, Codec::Vp9] {
1261            let mut session = stand_in(codec, "bcm2835-codec");
1262            assert_eq!(session.tag_sequence(&unit(VCE_SPS)), None, "{codec:?}");
1263        }
1264    }
1265
1266    /// The structures are laid out for the sizes the ioctl numbers encode, so a layout that
1267    /// drifted is caught here rather than by a device scribbling through a field.
1268    #[test]
1269    fn layouts_match_the_ioctl_numbers() {
1270        abi_matches().expect("the V4L2 structures are laid out as their ioctl numbers say");
1271    }
1272
1273    fn node(min: u32, max: u32, step: u32) -> NodeInfo {
1274        NodeInfo {
1275            path: "/dev/videoX".to_string(),
1276            driver: "test".to_string(),
1277            coded: vec![V4L2_PIX_FMT_H264],
1278            min_width: min,
1279            max_width: max,
1280            step_width: step,
1281            min_height: min,
1282            max_height: max,
1283            step_height: step,
1284        }
1285    }
1286
1287    /// A node is taken for what its capture queue enumerates, so one that encodes more than
1288    /// H.264 serves more than H.264 and one that encodes less is not asked for it.
1289    #[test]
1290    fn a_node_serves_the_codecs_it_enumerates() {
1291        let mut info = node(16, 1920, 2);
1292        info.coded = vec![V4L2_PIX_FMT_H264, V4L2_PIX_FMT_HEVC];
1293        assert!(info.serves(Codec::H264) && info.serves(Codec::H265));
1294        assert!(
1295            !info.serves(Codec::Vp8),
1296            "a format it does not enumerate is not served"
1297        );
1298        assert!(
1299            !info.serves(Codec::Av1),
1300            "and one the interface cannot name is never served"
1301        );
1302        info.coded = vec![V4L2_PIX_FMT_VP9];
1303        assert!(
1304            !info.serves(Codec::H264),
1305            "a node without H.264 is not asked for it"
1306        );
1307        assert!(info.serves(Codec::Vp9));
1308    }
1309
1310    /// The fourccs are the kernel's own, and an interface that cannot name a codec says so
1311    /// rather than guessing one.
1312    #[test]
1313    fn a_codec_maps_to_the_fourcc_the_kernel_names() {
1314        assert_eq!(
1315            coded_fourcc(Codec::H264),
1316            Some(u32::from_le_bytes(*b"H264"))
1317        );
1318        assert_eq!(
1319            coded_fourcc(Codec::H265),
1320            Some(u32::from_le_bytes(*b"HEVC"))
1321        );
1322        assert_eq!(coded_fourcc(Codec::Vp8), Some(u32::from_le_bytes(*b"VP80")));
1323        assert_eq!(coded_fourcc(Codec::Vp9), Some(u32::from_le_bytes(*b"VP90")));
1324        assert_eq!(coded_fourcc(Codec::Av1), None);
1325        assert_eq!(coded_fourcc(Codec::Jpeg), None);
1326    }
1327
1328    #[test]
1329    fn a_node_takes_the_sizes_inside_its_range_and_on_its_step() {
1330        let stepwise = node(32, 1920, 2);
1331        assert!(stepwise.fits(1920, 1080), "1080p is inside 32..1920 by 2");
1332        assert!(
1333            stepwise.fits(1920, 1200),
1334            "the ceiling is square, so 1920x1200 fits"
1335        );
1336        assert!(
1337            !stepwise.fits(2560, 1440),
1338            "a size past the ceiling does not fit"
1339        );
1340        assert!(
1341            !stepwise.fits(30, 30),
1342            "a size below the floor does not fit"
1343        );
1344        assert!(
1345            !stepwise.fits(1921, 1080),
1346            "an odd width is off a step of two"
1347        );
1348    }
1349
1350    /// A device that publishes one size answers for that size alone, which a step of zero says.
1351    #[test]
1352    fn a_discrete_node_takes_only_the_size_it_publishes() {
1353        let discrete = node(1280, 1280, 0);
1354        assert!(discrete.fits(1280, 1280));
1355        assert!(!discrete.fits(1281, 1280));
1356    }
1357
1358    #[test]
1359    fn the_input_format_follows_the_bytes_the_caller_holds() {
1360        assert_eq!(
1361            input_format(false),
1362            V4L2_PIX_FMT_BGRA,
1363            "an X11 capture hands over B,G,R,A"
1364        );
1365        assert_eq!(
1366            input_format(true),
1367            V4L2_PIX_FMT_RGBA,
1368            "an rgba caller hands over R,G,B,A"
1369        );
1370        assert_ne!(V4L2_PIX_FMT_BGRA, V4L2_PIX_FMT_RGBA);
1371    }
1372}
1373
1374/// Tests that need the device, in the spirit of the `gpu_` ones: run them with
1375/// `cargo test v4l2_ -- --ignored --test-threads=1` on a board that carries an M2M encode node.
1376/// They are serial by nature, since a node of one context refuses a second session.
1377#[cfg(test)]
1378mod hardware_tests {
1379    use super::*;
1380
1381    fn settings(width: i32, height: i32) -> RustCaptureSettings {
1382        RustCaptureSettings {
1383            width,
1384            height,
1385            codec: Codec::H264,
1386            video_bitrate_kbps: 8000,
1387            target_fps: 30.0,
1388            keyframe_interval_s: 2.0,
1389            omit_stripe_headers: true,
1390            ..Default::default()
1391        }
1392    }
1393
1394    /// A strip of primaries in the byte order a capture hands over: B, G, R, A.
1395    fn chart(width: usize, height: usize) -> Vec<u8> {
1396        let colors = [
1397            [255u8, 255, 255],
1398            [0, 0, 0],
1399            [0, 0, 255],
1400            [0, 255, 0],
1401            [255, 0, 0],
1402        ];
1403        let mut frame = Vec::with_capacity(width * height * 4);
1404        for _ in 0..height {
1405            for x in 0..width {
1406                let [b, g, r] = colors[(x * colors.len()) / width];
1407                frame.extend_from_slice(&[b, g, r, 255]);
1408            }
1409        }
1410        frame
1411    }
1412
1413    /// The NAL unit types an Annex B stream carries, in order.
1414    fn nal_types(stream: &[u8]) -> Vec<u8> {
1415        let mut types = Vec::new();
1416        let mut i = 0;
1417        while i + 3 < stream.len() {
1418            if stream[i] == 0 && stream[i + 1] == 0 && stream[i + 2] == 1 {
1419                types.push(stream[i + 3] & 0x1f);
1420                i += 4;
1421            } else {
1422                i += 1;
1423            }
1424        }
1425        types
1426    }
1427
1428    #[test]
1429    #[ignore]
1430    fn v4l2_node_answers_for_the_sizes_it_encodes() {
1431        assert!(available(), "no M2M encode node on this machine");
1432        assert!(
1433            encodes(Codec::H264, 1920, 1080),
1434            "1080p is inside every such device's range"
1435        );
1436        assert!(!encodes(Codec::H264, 0, 0), "a zero size is not encodable");
1437    }
1438
1439    #[test]
1440    #[ignore]
1441    fn v4l2_encodes_host_frames_to_h264() {
1442        let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(1280, 720), false)
1443            .expect("the session comes up");
1444        let frame = chart(1280, 720);
1445        let first = encoder
1446            .encode_host(&frame, 1280 * 4, false, 0, 26, true)
1447            .expect("first frame");
1448        let types = nal_types(&first);
1449        assert!(!first.is_empty(), "the first access unit is empty");
1450        assert!(
1451            types.contains(&7),
1452            "the key frame carries no SPS: {types:?}"
1453        );
1454        assert!(
1455            types.contains(&5),
1456            "the first frame is not an IDR: {types:?}"
1457        );
1458
1459        let second = encoder
1460            .encode_host(&frame, 1280 * 4, false, 1, 26, false)
1461            .expect("second");
1462        assert!(!second.is_empty(), "the second access unit is empty");
1463        assert!(
1464            second.len() < first.len(),
1465            "an unchanged frame coded no smaller than the key frame it follows: {} vs {}",
1466            second.len(),
1467            first.len()
1468        );
1469    }
1470
1471    /// The live rate change a dashboard slider makes, which a session must survive: a refusal
1472    /// here demotes the stream to software for good on the second occurrence.
1473    #[test]
1474    #[ignore]
1475    fn v4l2_takes_a_live_rate_change() {
1476        let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(1280, 720), false)
1477            .expect("the session comes up");
1478        let frame = chart(1280, 720);
1479        encoder
1480            .encode_host(&frame, 1280 * 4, false, 0, 26, true)
1481            .expect("first frame");
1482        let mut lowered = settings(1280, 720);
1483        lowered.video_bitrate_kbps = 1000;
1484        lowered.target_fps = 15.0;
1485        encoder
1486            .reconfigure_rate(&lowered)
1487            .expect("the device takes a live rate change");
1488        encoder
1489            .encode_host(&frame, 1280 * 4, false, 1, 26, false)
1490            .expect("a frame after it");
1491    }
1492
1493    /// What the device costs at 1080p, printed to quote rather than asserted: the frame rate it
1494    /// sustains one frame in one out, and the CPU that rate spends. The software figure it is
1495    /// worth against comes from a session, not from here — this path takes BGRA and the software
1496    /// one takes planes, so a comparison made here would leave out the conversion the software
1497    /// path pays for and measure two different jobs.
1498    #[test]
1499    #[ignore]
1500    fn v4l2_bench_1080p() {
1501        const FRAMES: u64 = 90;
1502        let (width, height) = (1920, 1080);
1503        let (w, h) = (width as usize, height as usize);
1504        // Three contents, because one number would misrepresent the device: a still desktop is
1505        // what it is best at, a desktop with a window moving is what it is asked for, and noise
1506        // is the worst case no screen produces.
1507        let contents: [(&str, Vec<Vec<u8>>); 3] = [
1508            ("still  ", vec![chart(w, h)]),
1509            ("moving ", (0..4).map(|n| shifted_chart(w, h, n)).collect()),
1510            ("noise  ", (0..4).map(|n| moving(w, h, n)).collect()),
1511        ];
1512        for (name, frames) in contents {
1513            let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(width, height), false)
1514                .expect("the session comes up");
1515            let cpu_before = process_cpu_seconds();
1516            let started = std::time::Instant::now();
1517            let mut bytes = 0usize;
1518            for n in 0..FRAMES {
1519                let frame = &frames[(n as usize) % frames.len()];
1520                bytes += encoder
1521                    .encode_host(frame, w * 4, false, n, 26, n == 0)
1522                    .expect("a frame")
1523                    .len();
1524            }
1525            let wall = started.elapsed().as_secs_f64();
1526            let cpu = process_cpu_seconds() - cpu_before;
1527            println!(
1528                "[v4l2-bench] {width}x{height} {name}: {:.1} fps serial, {:.2} ms per frame, \
1529                 CPU {:.2} ms per frame, {:.3} of a core at 30 fps, {:.0} kbit/s",
1530                FRAMES as f64 / wall,
1531                wall * 1000.0 / FRAMES as f64,
1532                cpu * 1000.0 / FRAMES as f64,
1533                cpu / FRAMES as f64 * 30.0,
1534                bytes as f64 * 8.0 / 1000.0 / (FRAMES as f64 / 30.0)
1535            );
1536        }
1537    }
1538
1539    /// CPU charged to this process, user and system, across every thread.
1540    fn process_cpu_seconds() -> f64 {
1541        let mut spec = libc::timespec {
1542            tv_sec: 0,
1543            tv_nsec: 0,
1544        };
1545        unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, &mut spec) };
1546        spec.tv_sec as f64 + spec.tv_nsec as f64 / 1e9
1547    }
1548
1549    /// The chart with its bars walked sideways, which is what a window being dragged looks like
1550    /// to an encoder: flat areas that move rather than pixels that change everywhere.
1551    fn shifted_chart(width: usize, height: usize, step: usize) -> Vec<u8> {
1552        let colors = [
1553            [255u8, 255, 255],
1554            [0, 0, 0],
1555            [0, 0, 255],
1556            [0, 255, 0],
1557            [255, 0, 0],
1558        ];
1559        let mut frame = Vec::with_capacity(width * height * 4);
1560        for _ in 0..height {
1561            for x in 0..width {
1562                let shifted = (x + step * 64) % width;
1563                let [b, g, r] = colors[(shifted * colors.len()) / width];
1564                frame.extend_from_slice(&[b, g, r, 255]);
1565            }
1566        }
1567        frame
1568    }
1569
1570    /// A frame whose content moves, so the encoder has something to spend bits on.
1571    fn moving(width: usize, height: usize, step: usize) -> Vec<u8> {
1572        let mut frame = Vec::with_capacity(width * height * 4);
1573        for y in 0..height {
1574            for x in 0..width {
1575                let value = ((x + step * 37) ^ (y + step * 11)) as u8;
1576                frame.extend_from_slice(&[
1577                    value,
1578                    value.wrapping_mul(3),
1579                    value.wrapping_mul(7),
1580                    255,
1581                ]);
1582            }
1583        }
1584        frame
1585    }
1586
1587    /// A rate the device will not take does not cost the session its hardware. A Raspberry Pi 4
1588    /// refuses 1080p at 60 and takes it at 30, and it says so only by refusing `STREAMON` on a
1589    /// descriptor that answers nothing afterwards, so the backend reopens and asks for less.
1590    #[test]
1591    #[ignore]
1592    fn v4l2_steps_down_from_a_rate_the_device_refuses() {
1593        let mut asked = settings(1920, 1080);
1594        asked.target_fps = 60.0;
1595        let encoder = V4l2M2mEncoder::new(Codec::H264, &asked, false)
1596            .expect("a session came up at some rate the device takes");
1597        assert!(
1598            encoder.fps <= 60.0,
1599            "the session reports a rate it never asked for"
1600        );
1601        assert!(encoder.fps >= 15.0, "the session stepped below the floor");
1602    }
1603
1604    /// What the stream declares, read back by a decoder that is not ours: OpenH264 decodes the
1605    /// stream and the sequence parameter set the session carries reports the signal in it.
1606    /// This is the check that a written set is a set a decoder accepts, rather than one our
1607    /// own reader agrees with.
1608    #[test]
1609    #[ignore]
1610    fn v4l2_the_color_reads_back_through_a_decoder() {
1611        use crate::webcam::decode::{ColorTags, Decoder as _, VideoDecoder};
1612
1613        let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(1280, 720), false)
1614            .expect("the session comes up");
1615        let frame = chart(1280, 720);
1616        let unit = encoder
1617            .encode_host(&frame, 1280 * 4, false, 0, 26, true)
1618            .expect("a frame");
1619        let mut decoder = VideoDecoder::new(Codec::H264).expect("a decoder");
1620        assert!(
1621            decoder.decode(&unit).expect("the stream decodes"),
1622            "no frame came back"
1623        );
1624
1625        let Some(signal) = encoder.signal() else {
1626            assert_eq!(
1627                decoder.color_tags().map(|tags| tags.matrix),
1628                Some(2),
1629                "the session claims nothing, so the stream must claim nothing either"
1630            );
1631            return;
1632        };
1633        assert_eq!(
1634            decoder.color_tags(),
1635            Some(ColorTags {
1636                matrix: signal.matrix,
1637                full_range: signal.full_range
1638            }),
1639            "the decoder reads a different signal than the session declares"
1640        );
1641    }
1642
1643    #[test]
1644    #[ignore]
1645    fn v4l2_refuses_a_size_past_the_ceiling() {
1646        let info = node().expect("a node");
1647        let too_wide = (info.max_width + info.step_width.max(2)) as i32;
1648        assert!(
1649            !encodes(Codec::H264, too_wide, 1080),
1650            "a size past the ceiling reported as encodable"
1651        );
1652        let error = match V4l2M2mEncoder::new(Codec::H264, &settings(too_wide, 1080), false) {
1653            Ok(_) => panic!("a session past the ceiling came up"),
1654            Err(e) => e,
1655        };
1656        assert!(
1657            error.contains("encodes"),
1658            "the refusal does not name the range: {error}"
1659        );
1660    }
1661}