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}