ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
camera_probe.cpp
Go to the documentation of this file.
1#include "camera_probe.hpp"
2
3#include <algorithm>
4#include <array>
5#include <cerrno>
6#include <cmath>
7#include <cstdint>
8#include <cstring>
9#include <filesystem>
10#include <fstream>
11#include <functional>
12#include <iomanip>
13#include <ostream>
14#include <string>
15#include <vector>
16
17#ifdef __linux__
18#include <fcntl.h>
19#include <linux/videodev2.h>
20#include <sys/ioctl.h>
21#include <unistd.h>
22#endif
23
24namespace acmxvk {
25 namespace fs = std::filesystem;
26 namespace {
27#ifdef __linux__
28 [[nodiscard]] std::string sanitizeDeviceText(std::string text) {
29 for (char &character : text) {
30 const unsigned char value =
31 static_cast<unsigned char>(character);
32 if (value < 0x20U || value == 0x7FU) {
33 character = ' ';
34 }
35 }
36 return text;
37 }
38
39 template <std::size_t Size>
40 [[nodiscard]] std::string v4l2Text(const __u8 (&value)[Size]) {
41 const char *text = reinterpret_cast<const char *>(value);
42 return sanitizeDeviceText(
43 std::string(text, ::strnlen(text, Size)));
44 }
45
46 [[nodiscard]] int cameraIoctl(int descriptor, unsigned long request,
47 void *argument) {
48 int result = -1;
49 do {
50 result = ioctl(descriptor, request, argument);
51 } while (result < 0 && errno == EINTR);
52 return result;
53 }
54
55 void appendFrameRate(std::vector<double> &frame_rates,
56 double frame_rate) {
57 if (!std::isfinite(frame_rate) || frame_rate <= 0.0) {
58 return;
59 }
60 const auto existing = std::find_if(
61 frame_rates.begin(), frame_rates.end(),
62 [frame_rate](double value) {
63 return std::abs(value - frame_rate) < 0.05;
64 });
65 if (existing == frame_rates.end()) {
66 frame_rates.push_back(frame_rate);
67 }
68 }
69
70 [[nodiscard]] double frameRate(const v4l2_fract &interval) {
71 if (interval.numerator == 0U) {
72 return 0.0;
73 }
74 return static_cast<double>(interval.denominator) /
75 static_cast<double>(interval.numerator);
76 }
77
78 [[nodiscard]] std::string fourccName(std::uint32_t fourcc) {
79 std::array<char, 5> name{
80 static_cast<char>(fourcc & 0xFFU),
81 static_cast<char>((fourcc >> 8U) & 0xFFU),
82 static_cast<char>((fourcc >> 16U) & 0xFFU),
83 static_cast<char>((fourcc >> 24U) & 0xFFU), '\0'};
84 for (std::size_t index = 0; index < 4U; ++index) {
85 const unsigned char character =
86 static_cast<unsigned char>(name[index]);
87 if (character < 0x20U || character > 0x7EU) {
88 name[index] = '?';
89 }
90 }
91 return name.data();
92 }
93
94 [[nodiscard]] bool enumerateCaptureType(
95 int descriptor, v4l2_buf_type capture_type, bool loopback_device,
96 double current_fps, std::ostream &output) {
97 bool found_format = false;
98 v4l2_fmtdesc format{};
99 format.type = capture_type;
100 for (format.index = 0U;
101 cameraIoctl(descriptor, VIDIOC_ENUM_FMT, &format) == 0;
102 ++format.index) {
103 found_format = true;
104 output << "\n Format: " << fourccName(format.pixelformat)
105 << " (" << v4l2Text(format.description) << ")\n";
106
107 v4l2_frmsizeenum frame_size{};
108 frame_size.pixel_format = format.pixelformat;
109 for (frame_size.index = 0U;
110 cameraIoctl(descriptor, VIDIOC_ENUM_FRAMESIZES,
111 &frame_size) == 0;
112 ++frame_size.index) {
113 if (frame_size.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
114 std::vector<double> frame_rates;
115 v4l2_frmivalenum interval{};
116 interval.pixel_format = format.pixelformat;
117 interval.width = frame_size.discrete.width;
118 interval.height = frame_size.discrete.height;
119 for (interval.index = 0U;
120 cameraIoctl(descriptor,
121 VIDIOC_ENUM_FRAMEINTERVALS,
122 &interval) == 0;
123 ++interval.index) {
124 if (interval.type ==
125 V4L2_FRMIVAL_TYPE_DISCRETE) {
126 appendFrameRate(frame_rates,
127 frameRate(interval.discrete));
128 } else if (interval.type ==
129 V4L2_FRMIVAL_TYPE_STEPWISE ||
130 interval.type ==
131 V4L2_FRMIVAL_TYPE_CONTINUOUS) {
132 appendFrameRate(
133 frame_rates,
134 frameRate(interval.stepwise.min));
135 appendFrameRate(
136 frame_rates,
137 frameRate(interval.stepwise.max));
138 break;
139 }
140 }
141
142 if (frame_rates.empty()) {
143 appendFrameRate(frame_rates, current_fps);
144 }
145 if (loopback_device) {
146 constexpr std::array<double, 9>
147 LOOPBACK_FRAME_RATES{24.0, 25.0, 30.0,
148 50.0, 60.0, 90.0,
149 120.0, 144.0, 240.0};
150 for (double frame_rate : LOOPBACK_FRAME_RATES) {
151 appendFrameRate(frame_rates, frame_rate);
152 }
153 }
154 std::sort(frame_rates.begin(), frame_rates.end(),
155 std::greater<double>());
156
157 output << " " << frame_size.discrete.width << 'x'
158 << frame_size.discrete.height;
159 bool first = true;
160 for (double frame_rate : frame_rates) {
161 output << (first ? " @ " : ", ") << std::fixed
162 << std::setprecision(1) << frame_rate
163 << " fps";
164 first = false;
165 }
166 output << '\n';
167 } else if (frame_size.type ==
168 V4L2_FRMSIZE_TYPE_STEPWISE ||
169 frame_size.type ==
170 V4L2_FRMSIZE_TYPE_CONTINUOUS) {
171 output << " " << frame_size.stepwise.min_width
172 << 'x' << frame_size.stepwise.min_height
173 << " to " << frame_size.stepwise.max_width
174 << 'x' << frame_size.stepwise.max_height
175 << " (step " << frame_size.stepwise.step_width
176 << 'x' << frame_size.stepwise.step_height
177 << ")\n";
178 break;
179 }
180 }
181 }
182 return found_format;
183 }
184
185 [[nodiscard]] bool primaryVideoNode(int device_index) {
186 std::ifstream index_file(
187 "/sys/class/video4linux/video" +
188 std::to_string(device_index) + "/index");
189 int stream_index = 0;
190 return !(index_file >> stream_index) || stream_index == 0;
191 }
192#endif
193 } // namespace
194
195 [[nodiscard]] bool listCameraDevices(std::ostream &output,
196 std::ostream &error) {
197#ifdef __linux__
198 std::vector<int> device_indices;
199 std::error_code directory_error;
200 for (const fs::directory_entry &entry :
201 fs::directory_iterator("/dev", directory_error)) {
202 const std::string name = entry.path().filename().string();
203 if (!name.starts_with("video") || name.size() <= 5U ||
204 !std::all_of(name.begin() + 5, name.end(), [](char character) {
205 return character >= '0' && character <= '9';
206 })) {
207 continue;
208 }
209 try {
210 const int index = std::stoi(name.substr(5));
211 if (index >= 0 && primaryVideoNode(index)) {
212 device_indices.push_back(index);
213 }
214 } catch (const std::exception &) {
215 continue;
216 }
217 }
218 if (directory_error) {
219 error << "acmxvk: cannot enumerate /dev video devices: "
220 << directory_error.message() << '\n';
221 return false;
222 }
223 std::sort(device_indices.begin(), device_indices.end());
224 device_indices.erase(
225 std::unique(device_indices.begin(), device_indices.end()),
226 device_indices.end());
227
228 bool found_device = false;
229 for (int device_index : device_indices) {
230 const std::string path =
231 "/dev/video" + std::to_string(device_index);
232 int descriptor =
233 open(path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC);
234 if (descriptor < 0) {
235 continue;
236 }
237 v4l2_capability capability{};
238 const bool queried =
239 cameraIoctl(descriptor, VIDIOC_QUERYCAP, &capability) == 0;
240 close(descriptor);
241 if (!queried) {
242 continue;
243 }
244 const std::uint32_t capabilities =
245 (capability.capabilities & V4L2_CAP_DEVICE_CAPS) != 0U
246 ? capability.device_caps
247 : capability.capabilities;
248 if ((capabilities & (V4L2_CAP_VIDEO_CAPTURE |
249 V4L2_CAP_VIDEO_CAPTURE_MPLANE)) == 0U) {
250 continue;
251 }
252 output << device_index << '\t' << v4l2Text(capability.card)
253 << '\n';
254 found_device = true;
255 }
256 if (!found_device) {
257 error << "acmxvk: no V4L2 capture devices found\n";
258 }
259 return found_device;
260#else
261 static_cast<void>(output);
262 error << "acmxvk: --list-camera-devices is supported on Linux and macOS\n";
263 return false;
264#endif
265 }
266
267 [[nodiscard]] bool probeCameraDevice(int device_index,
268 std::ostream &output,
269 std::ostream &error) {
270#ifdef __linux__
271 const std::string device_path =
272 "/dev/video" + std::to_string(device_index);
273 int descriptor =
274 open(device_path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC);
275 if (descriptor < 0) {
276 descriptor =
277 open(device_path.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC);
278 }
279 if (descriptor < 0) {
280 error << "acmxvk: cannot open " << device_path << ": "
281 << std::strerror(errno) << '\n';
282 return false;
283 }
284
285 struct DescriptorGuard {
286 int value;
287 ~DescriptorGuard() {
288 if (value >= 0) {
289 close(value);
290 }
291 }
292 } guard{descriptor};
293
294 v4l2_capability capability{};
295 if (cameraIoctl(descriptor, VIDIOC_QUERYCAP, &capability) != 0) {
296 error << "acmxvk: cannot query " << device_path << ": "
297 << std::strerror(errno) << '\n';
298 return false;
299 }
300
301 const std::uint32_t device_capabilities =
302 (capability.capabilities & V4L2_CAP_DEVICE_CAPS) != 0U
303 ? capability.device_caps
304 : capability.capabilities;
305 const std::string driver = v4l2Text(capability.driver);
306 const bool loopback_device =
307 driver.find("v4l2loopback") != std::string::npos ||
308 driver.find("v4l2 loopback") != std::string::npos;
309
310 output << "Device " << device_index << ": " << device_path << '\n'
311 << " Driver : " << driver << '\n'
312 << " Card : " << v4l2Text(capability.card) << '\n'
313 << " Bus : " << v4l2Text(capability.bus_info) << '\n';
314
315 double current_fps = 0.0;
316 v4l2_streamparm stream_parameters{};
317 stream_parameters.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
318 if (cameraIoctl(descriptor, VIDIOC_G_PARM, &stream_parameters) == 0) {
319 current_fps =
320 frameRate(stream_parameters.parm.capture.timeperframe);
321 }
322
323 bool found_format = false;
324 if ((device_capabilities & V4L2_CAP_VIDEO_CAPTURE) != 0U) {
325 found_format = enumerateCaptureType(
326 descriptor, V4L2_BUF_TYPE_VIDEO_CAPTURE,
327 loopback_device, current_fps, output) ||
328 found_format;
329 }
330 if ((device_capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) != 0U) {
331 found_format = enumerateCaptureType(
332 descriptor, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
333 loopback_device, current_fps, output) ||
334 found_format;
335 }
336 if (!found_format) {
337 error << "acmxvk: " << device_path
338 << " exposes no capture formats\n";
339 }
340 return found_format;
341#else
342 static_cast<void>(device_index);
343 static_cast<void>(output);
344 error << "acmxvk: --enumerate-device is supported on Linux and macOS\n";
345 return false;
346#endif
347 }
348} // namespace acmxvk
bool listCameraDevices(std::ostream &output, std::ostream &error)
Print native camera device indices and names as tab-separated records.
bool probeCameraDevice(int device_index, std::ostream &output, std::ostream &error)
Print the native capture formats, resolutions, and frame rates exposed by a camera device.