Skip to main content

denise_video/
detect.rs

1//! What this board's hardware decodes, asked rather than guessed.
2//!
3//! A V4L2 decoder *enumerates* the compressed formats it accepts
4//! (`VIDIOC_ENUM_FMT` on the output queue), so detection is a walk over
5//! `/dev/video*` — the same walk the `probe` example prints. On a Pi this
6//! finds `bcm2835-codec-decode` (H.264, Pi Zero through 4) and `rpivid`
7//! (HEVC, Pi 4 and 5); on other SoCs, whatever their vendor shipped.
8
9use std::path::{Path, PathBuf};
10
11use crate::annexb::Codec;
12use crate::v4l2;
13
14/// Where video nodes live.
15const DEV_DIR: &str = "/dev";
16
17/// One decodable asset an application offers: a codec and the file that holds
18/// its elementary stream.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct Asset {
21    /// Which of the menu's two codecs the file contains.
22    pub codec: Codec,
23    /// The elementary stream (`.h264` / `.h265`).
24    pub path: PathBuf,
25}
26
27impl Asset {
28    /// An H.264 elementary stream.
29    pub fn h264(path: impl Into<PathBuf>) -> Self {
30        Self {
31            codec: Codec::H264,
32            path: path.into(),
33        }
34    }
35
36    /// An HEVC elementary stream.
37    pub fn h265(path: impl Into<PathBuf>) -> Self {
38        Self {
39            codec: Codec::H265,
40            path: path.into(),
41        }
42    }
43}
44
45/// One decoder node and what it accepts.
46#[derive(Clone, Debug)]
47pub struct DecoderInfo {
48    /// The device node, `/dev/videoN`.
49    pub path: PathBuf,
50    /// The driver's name, as it reports it.
51    pub driver: String,
52    /// Whether the compressed queue accepts H.264.
53    pub h264: bool,
54    /// Whether the compressed queue accepts HEVC.
55    pub hevc: bool,
56    /// Whether the decoder is **stateful** — feed bytes, frames come out.
57    ///
58    /// A stateless decoder (`rpivid`) advertises the codec but needs
59    /// userspace to parse slices and drive the request API; this crate's
60    /// stateful path must not be pointed at one. Heuristic: stateless
61    /// drivers expose their controls and are known by name.
62    pub stateful: bool,
63}
64
65/// The board's decoders, enumerated once.
66#[derive(Clone, Debug, Default)]
67pub struct Decoders {
68    /// Every M2M decoder found, in `/dev/video*` order.
69    pub found: Vec<DecoderInfo>,
70}
71
72impl Decoders {
73    /// Walks `/dev/video*` and asks each node what it is.
74    ///
75    /// Nodes that refuse to open or are not memory-to-memory decoders are
76    /// skipped silently — a camera is not an error, it is a camera.
77    pub fn detect() -> Self {
78        Self::detect_in(DEV_DIR)
79    }
80
81    /// [`Decoders::detect`] against an alternate `/dev`, for tests.
82    pub fn detect_in(dev: impl AsRef<Path>) -> Self {
83        let mut found = Vec::new();
84        let Ok(entries) = std::fs::read_dir(dev.as_ref()) else {
85            return Self { found };
86        };
87        let mut paths: Vec<PathBuf> = entries
88            .filter_map(|e| e.ok())
89            .map(|e| e.path())
90            .filter(|p| {
91                p.file_name()
92                    .and_then(|n| n.to_str())
93                    .is_some_and(|n| n.starts_with("video") && n[5..].parse::<u32>().is_ok())
94            })
95            .collect();
96        paths.sort();
97        for path in paths {
98            if let Some(info) = Self::inspect(&path) {
99                found.push(info);
100            }
101        }
102        Self { found }
103    }
104
105    /// Asks one node whether it is a decoder, and for what.
106    fn inspect(path: &Path) -> Option<DecoderInfo> {
107        use std::os::fd::AsFd;
108        let file = std::fs::OpenOptions::new()
109            .read(true)
110            .write(true)
111            .open(path)
112            .ok()?;
113        let fd = file.as_fd();
114        let cap = v4l2::querycap(fd).ok()?;
115        let caps = cap.caps();
116        if caps & v4l2::CAP_VIDEO_M2M_MPLANE == 0 || caps & v4l2::CAP_STREAMING == 0 {
117            return None;
118        }
119        let (mut h264, mut hevc, mut any_compressed) = (false, false, false);
120        for index in 0.. {
121            match v4l2::enum_fmt(fd, v4l2::BUF_TYPE_OUTPUT_MPLANE, index).ok()? {
122                None => break,
123                Some(desc) => {
124                    if desc.flags & v4l2::FMT_FLAG_COMPRESSED != 0 {
125                        any_compressed = true;
126                    }
127                    match desc.pixelformat {
128                        v4l2::PIX_FMT_H264 => h264 = true,
129                        v4l2::PIX_FMT_HEVC => hevc = true,
130                        _ => {}
131                    }
132                }
133            }
134        }
135        // A decoder takes compressed in; an encoder takes it out. A node with
136        // no compressed input format is not a decoder.
137        if !any_compressed {
138            return None;
139        }
140        let driver = cap.driver_name().to_owned();
141        // The known stateless drivers. Wrong-by-default is the safe polarity:
142        // an unknown stateless driver marked stateful fails loudly at S_FMT,
143        // where an unknown stateful driver marked stateless would silently
144        // never be used.
145        let stateful = !matches!(
146            driver.as_str(),
147            "rpivid" | "hantro-vpu" | "rkvdec" | "cedrus"
148        );
149        Some(DecoderInfo {
150            path: path.to_path_buf(),
151            driver,
152            h264,
153            hevc,
154            stateful,
155        })
156    }
157
158    /// The node the **stateful** path uses for `codec`, if any.
159    pub fn stateful_for(&self, codec: Codec) -> Option<&DecoderInfo> {
160        self.found.iter().find(|d| {
161            d.stateful
162                && match codec {
163                    Codec::H264 => d.h264,
164                    Codec::H265 => d.hevc,
165                }
166        })
167    }
168
169    /// The menu's rule: the first offered asset this board's stateful path
170    /// plays.
171    ///
172    /// A kiosk offers both files; the board picks. Order expresses the
173    /// application's preference when a board plays both.
174    pub fn pick<'a>(&self, assets: &'a [Asset]) -> Option<(&'a Asset, &DecoderInfo)> {
175        assets
176            .iter()
177            .find_map(|asset| self.stateful_for(asset.codec).map(|d| (asset, d)))
178    }
179}