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) {
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)));
46 [[nodiscard]]
int cameraIoctl(
int descriptor,
unsigned long request,
50 result = ioctl(descriptor, request, argument);
51 }
while (result < 0 && errno == EINTR);
55 void appendFrameRate(std::vector<double> &frame_rates,
57 if (!std::isfinite(frame_rate) || frame_rate <= 0.0) {
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;
65 if (existing == frame_rates.end()) {
66 frame_rates.push_back(frame_rate);
70 [[nodiscard]]
double frameRate(
const v4l2_fract &interval) {
71 if (interval.numerator == 0U) {
74 return static_cast<double>(interval.denominator) /
75 static_cast<double>(interval.numerator);
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) {
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;
104 output <<
"\n Format: " << fourccName(format.pixelformat)
105 <<
" (" << v4l2Text(format.description) <<
")\n";
107 v4l2_frmsizeenum frame_size{};
108 frame_size.pixel_format = format.pixelformat;
109 for (frame_size.index = 0U;
110 cameraIoctl(descriptor, VIDIOC_ENUM_FRAMESIZES,
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,
125 V4L2_FRMIVAL_TYPE_DISCRETE) {
126 appendFrameRate(frame_rates,
127 frameRate(interval.discrete));
128 }
else if (interval.type ==
129 V4L2_FRMIVAL_TYPE_STEPWISE ||
131 V4L2_FRMIVAL_TYPE_CONTINUOUS) {
134 frameRate(interval.stepwise.min));
137 frameRate(interval.stepwise.max));
142 if (frame_rates.empty()) {
143 appendFrameRate(frame_rates, current_fps);
145 if (loopback_device) {
146 constexpr std::array<double, 9>
147 LOOPBACK_FRAME_RATES{24.0, 25.0, 30.0,
149 120.0, 144.0, 240.0};
150 for (
double frame_rate : LOOPBACK_FRAME_RATES) {
151 appendFrameRate(frame_rates, frame_rate);
154 std::sort(frame_rates.begin(), frame_rates.end(),
155 std::greater<double>());
157 output <<
" " << frame_size.discrete.width <<
'x'
158 << frame_size.discrete.height;
160 for (
double frame_rate : frame_rates) {
161 output << (first ?
" @ " :
", ") << std::fixed
162 << std::setprecision(1) << frame_rate
167 }
else if (frame_size.type ==
168 V4L2_FRMSIZE_TYPE_STEPWISE ||
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
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;
196 std::ostream &error) {
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';
210 const int index = std::stoi(name.substr(5));
211 if (index >= 0 && primaryVideoNode(index)) {
212 device_indices.push_back(index);
214 }
catch (
const std::exception &) {
218 if (directory_error) {
219 error <<
"acmxvk: cannot enumerate /dev video devices: "
220 << directory_error.message() <<
'\n';
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());
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);
233 open(path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC);
234 if (descriptor < 0) {
237 v4l2_capability capability{};
239 cameraIoctl(descriptor, VIDIOC_QUERYCAP, &capability) == 0;
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) {
252 output << device_index <<
'\t' << v4l2Text(capability.card)
257 error <<
"acmxvk: no V4L2 capture devices found\n";
261 static_cast<void>(output);
262 error <<
"acmxvk: --list-camera-devices is supported on Linux and macOS\n";
268 std::ostream &output,
269 std::ostream &error) {
271 const std::string device_path =
272 "/dev/video" + std::to_string(device_index);
274 open(device_path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC);
275 if (descriptor < 0) {
277 open(device_path.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC);
279 if (descriptor < 0) {
280 error <<
"acmxvk: cannot open " << device_path <<
": "
281 << std::strerror(errno) <<
'\n';
285 struct DescriptorGuard {
294 v4l2_capability capability{};
295 if (cameraIoctl(descriptor, VIDIOC_QUERYCAP, &capability) != 0) {
296 error <<
"acmxvk: cannot query " << device_path <<
": "
297 << std::strerror(errno) <<
'\n';
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;
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';
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) {
320 frameRate(stream_parameters.parm.capture.timeperframe);
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) ||
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) ||
337 error <<
"acmxvk: " << device_path
338 <<
" exposes no capture formats\n";
342 static_cast<void>(device_index);
343 static_cast<void>(output);
344 error <<
"acmxvk: --enumerate-device is supported on Linux and macOS\n";