GCC Code Coverage Report


Directory: ./
File: mediaoutput/V4L2Device.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 0 203 0.0%
Functions: 0 13 0.0%
Branches: 0 303 0.0%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "V4L2Device.h"
16 #include "log.h"
17
18 #if __has_include(<linux/videodev2.h>)
19 #define HAS_V4L2
20 #include <errno.h>
21 #include <fcntl.h>
22 #include <linux/videodev2.h>
23 #include <string.h>
24 #include <sys/ioctl.h>
25 #include <unistd.h>
26 #endif
27
28 #include <algorithm>
29 #include <cstdio>
30
31 namespace V4L2Device {
32
33 #ifdef HAS_V4L2
34
35 // ioctl() on a video node returns EINTR often enough to matter (any signal
36 // delivered to the calling thread will do it), and a spurious failure here
37 // silently drops an anti-flicker setting.
38 ✗ static int xioctl(int fd, unsigned long req, void* arg) {
39 int r;
40 do {
41 ✗ r = ioctl(fd, req, arg);
42 ✗ } while (r == -1 && errno == EINTR);
43 ✗ return r;
44 }
45
46 // Opened O_RDWR because S_CTRL needs write access, but never for streaming:
47 // UVC allows any number of non-streaming openers, so this does not contend
48 // with the capture that v4l2src is already running on the same node.
49 // O_NONBLOCK keeps a wedged device from hanging the caller.
50 ✗ static int OpenDevice(const std::string& device) {
51 ✗ int fd = open(device.c_str(), O_RDWR | O_NONBLOCK);
52 ✗ if (fd < 0) {
53 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: cannot open %s: %s\n", device.c_str(), strerror(errno));
54 }
55 ✗ return fd;
56 }
57
58 /// Is this control present and settable right now?
59 ✗ static bool QueryControl(int fd, unsigned int cid, v4l2_queryctrl& qc) {
60 ✗ memset(&qc, 0, sizeof(qc));
61 ✗ qc.id = cid;
62 ✗ if (xioctl(fd, VIDIOC_QUERYCTRL, &qc) != 0)
63 ✗ return false;
64 // DISABLED means the control exists but cannot currently be set (e.g.
65 // exposure time while auto-exposure owns it) -- callers that can fix
66 // that ordering re-query rather than treating it as absent.
67 ✗ return !(qc.flags & V4L2_CTRL_FLAG_DISABLED);
68 }
69
70 ✗ static bool MenuHasValue(int fd, unsigned int cid, int value) {
71 v4l2_querymenu qm;
72 ✗ memset(&qm, 0, sizeof(qm));
73 ✗ qm.id = cid;
74 ✗ qm.index = value;
75 ✗ return xioctl(fd, VIDIOC_QUERYMENU, &qm) == 0;
76 }
77
78 ✗ static bool SetControl(int fd, unsigned int cid, int value) {
79 v4l2_control ctrl;
80 ✗ memset(&ctrl, 0, sizeof(ctrl));
81 ✗ ctrl.id = cid;
82 ✗ ctrl.value = value;
83 ✗ return xioctl(fd, VIDIOC_S_CTRL, &ctrl) == 0;
84 }
85
86 ✗ static void Append(std::string& summary, const std::string& item) {
87 ✗ if (!summary.empty())
88 ✗ summary += ", ";
89 ✗ summary += item;
90 ✗ }
91
92 ✗ bool ApplyControls(const std::string& device, const ControlSettings& s, std::string& summary) {
93 ✗ summary.clear();
94 ✗ if (!s.AnyRequested())
95 ✗ return true;
96
97 ✗ int fd = OpenDevice(device);
98 ✗ if (fd < 0)
99 ✗ return false;
100
101 v4l2_queryctrl qc;
102
103 // Order matters. power_line_frequency and exposure_dynamic_framerate
104 // both steer the auto-exposure algorithm, so they go in before the
105 // exposure mode is decided; exposure_time_absolute goes in last because
106 // the driver keeps it DISABLED until manual mode is actually active.
107
108 ✗ if (s.powerLineFrequency >= 0) {
109 ✗ if (!QueryControl(fd, V4L2_CID_POWER_LINE_FREQUENCY, qc)) {
110 ✗ LogInfo(VB_MEDIAOUT, "V4L2Device: %s has no power_line_frequency control\n", device.c_str());
111 ✗ } else if (s.powerLineFrequency > qc.maximum || s.powerLineFrequency < qc.minimum) {
112 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s does not support power_line_frequency=%d (range %d-%d)\n",
113 device.c_str(), s.powerLineFrequency, qc.minimum, qc.maximum);
114 ✗ } else if (SetControl(fd, V4L2_CID_POWER_LINE_FREQUENCY, s.powerLineFrequency)) {
115 ✗ Append(summary, "power_line_frequency=" + std::to_string(s.powerLineFrequency));
116 } else {
117 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s failed to set power_line_frequency=%d: %s\n",
118 device.c_str(), s.powerLineFrequency, strerror(errno));
119 }
120 }
121
122 ✗ if (s.dynamicFramerate >= 0) {
123 ✗ if (!QueryControl(fd, V4L2_CID_EXPOSURE_AUTO_PRIORITY, qc)) {
124 ✗ LogInfo(VB_MEDIAOUT, "V4L2Device: %s has no exposure_dynamic_framerate control\n", device.c_str());
125 ✗ } else if (SetControl(fd, V4L2_CID_EXPOSURE_AUTO_PRIORITY, s.dynamicFramerate ? 1 : 0)) {
126 ✗ Append(summary, "exposure_dynamic_framerate=" + std::to_string(s.dynamicFramerate ? 1 : 0));
127 } else {
128 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s failed to set exposure_dynamic_framerate: %s\n",
129 device.c_str(), strerror(errno));
130 }
131 }
132
133 ✗ if (s.exposureMode == "auto" || s.exposureMode == "manual") {
134 ✗ if (!QueryControl(fd, V4L2_CID_EXPOSURE_AUTO, qc)) {
135 ✗ LogInfo(VB_MEDIAOUT, "V4L2Device: %s has no auto_exposure control\n", device.c_str());
136 } else {
137 // The menu is sparse: UVC cameras typically expose only MANUAL(1)
138 // and APERTURE_PRIORITY(3), not AUTO(0) or SHUTTER_PRIORITY(2).
139 // Probe rather than assume, so "auto" means whichever automatic
140 // mode this camera actually has.
141 ✗ int want = -1;
142 ✗ if (s.exposureMode == "manual") {
143 ✗ if (MenuHasValue(fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_MANUAL))
144 ✗ want = V4L2_EXPOSURE_MANUAL;
145 ✗ else if (MenuHasValue(fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_SHUTTER_PRIORITY))
146 ✗ want = V4L2_EXPOSURE_SHUTTER_PRIORITY;
147 } else {
148 ✗ if (MenuHasValue(fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_APERTURE_PRIORITY))
149 ✗ want = V4L2_EXPOSURE_APERTURE_PRIORITY;
150 ✗ else if (MenuHasValue(fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_AUTO))
151 ✗ want = V4L2_EXPOSURE_AUTO;
152 }
153
154 ✗ if (want < 0) {
155 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s offers no '%s' exposure mode\n",
156 device.c_str(), s.exposureMode.c_str());
157 ✗ } else if (SetControl(fd, V4L2_CID_EXPOSURE_AUTO, want)) {
158 ✗ Append(summary, "auto_exposure=" + std::to_string(want) + " (" + s.exposureMode + ")");
159 } else {
160 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s failed to set auto_exposure=%d: %s\n",
161 device.c_str(), want, strerror(errno));
162 }
163 }
164 }
165
166 ✗ if (s.exposureMode == "manual" && s.exposureTime100us >= 0) {
167 // Re-queried after the mode switch above: it reads back DISABLED
168 // while auto-exposure owns it, and settable once manual is active.
169 ✗ if (!QueryControl(fd, V4L2_CID_EXPOSURE_ABSOLUTE, qc)) {
170 ✗ LogInfo(VB_MEDIAOUT, "V4L2Device: %s exposure_time_absolute unavailable (manual mode not active?)\n",
171 device.c_str());
172 } else {
173 ✗ int v = s.exposureTime100us;
174 ✗ if (v < qc.minimum)
175 ✗ v = qc.minimum;
176 ✗ if (v > qc.maximum)
177 ✗ v = qc.maximum;
178 ✗ if (v != s.exposureTime100us) {
179 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s clamped exposure_time_absolute %d -> %d (range %d-%d)\n",
180 device.c_str(), s.exposureTime100us, v, qc.minimum, qc.maximum);
181 }
182 ✗ if (SetControl(fd, V4L2_CID_EXPOSURE_ABSOLUTE, v)) {
183 ✗ Append(summary, "exposure_time_absolute=" + std::to_string(v));
184 } else {
185 ✗ LogWarn(VB_MEDIAOUT, "V4L2Device: %s failed to set exposure_time_absolute=%d: %s\n",
186 device.c_str(), v, strerror(errno));
187 }
188 }
189 }
190
191 ✗ close(fd);
192 ✗ return true;
193 }
194
195 ✗ static std::string FourccToString(unsigned int f) {
196 ✗ char buf[5] = { (char)(f & 0xFF), (char)((f >> 8) & 0xFF),
197 ✗ (char)((f >> 16) & 0xFF), (char)((f >> 24) & 0xFF), 0 };
198 ✗ return std::string(buf);
199 }
200
201 /// Framerates for one pixelformat/size, appended to `out` as full Modes.
202 ✗ static void EnumIntervals(int fd, unsigned int pixfmt, const std::string& fourcc,
203 int w, int h, std::vector<Mode>& out) {
204 ✗ const size_t before = out.size();
205 v4l2_frmivalenum fi;
206 ✗ memset(&fi, 0, sizeof(fi));
207 ✗ fi.pixel_format = pixfmt;
208 ✗ fi.width = w;
209 ✗ fi.height = h;
210
211 ✗ for (fi.index = 0; xioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fi) == 0; fi.index++) {
212 ✗ Mode m;
213 ✗ m.fourcc = fourcc;
214 ✗ m.width = w;
215 ✗ m.height = h;
216 ✗ if (fi.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
217 // An *interval* is seconds-per-frame, so the framerate is its
218 // reciprocal -- numerator and denominator swap here.
219 ✗ if (fi.discrete.numerator == 0)
220 ✗ continue;
221 ✗ m.fpsNum = fi.discrete.denominator;
222 ✗ m.fpsDen = fi.discrete.numerator;
223 } else {
224 // Stepwise/continuous: the shortest interval is the highest
225 // framerate, which is the only one worth advertising.
226 ✗ if (fi.stepwise.min.numerator == 0)
227 ✗ continue;
228 ✗ m.fpsNum = fi.stepwise.min.denominator;
229 ✗ m.fpsDen = fi.stepwise.min.numerator;
230 ✗ out.push_back(m);
231 ✗ break;
232 }
233 ✗ out.push_back(m);
234 ✗ }
235
236 // A device that enumerates the size but refuses to enumerate intervals
237 // still has that size; record it with an unknown rate so mode selection
238 // can pin the resolution and leave the framerate to the device.
239 // Keyed on "did this call add anything", not on inspecting out.back():
240 // the same size is commonly enumerated under several pixelformats, and
241 // the previous one's entry would otherwise look like this one's.
242 ✗ if (out.size() == before) {
243 ✗ Mode m;
244 ✗ m.fourcc = fourcc;
245 ✗ m.width = w;
246 ✗ m.height = h;
247 ✗ m.fpsNum = 0;
248 ✗ m.fpsDen = 1;
249 ✗ out.push_back(m);
250 ✗ }
251 ✗ }
252
253 ✗ std::vector<Mode> EnumerateModes(const std::string& device) {
254 ✗ std::vector<Mode> modes;
255 ✗ int fd = OpenDevice(device);
256 ✗ if (fd < 0)
257 ✗ return modes;
258
259 v4l2_fmtdesc fmt;
260 ✗ memset(&fmt, 0, sizeof(fmt));
261 ✗ fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
262
263 ✗ for (fmt.index = 0; xioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0; fmt.index++) {
264 ✗ std::string fourcc = FourccToString(fmt.pixelformat);
265
266 v4l2_frmsizeenum fs;
267 ✗ memset(&fs, 0, sizeof(fs));
268 ✗ fs.pixel_format = fmt.pixelformat;
269
270 ✗ for (fs.index = 0; xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fs) == 0; fs.index++) {
271 ✗ if (fs.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
272 ✗ EnumIntervals(fd, fmt.pixelformat, fourcc,
273 ✗ fs.discrete.width, fs.discrete.height, modes);
274 } else {
275 // Stepwise sizes: take the maximum as the single
276 // representative. Anything smaller is reachable by
277 // videoscale anyway, and guessing at steps risks asking
278 // for a size the driver rounds somewhere else.
279 ✗ EnumIntervals(fd, fmt.pixelformat, fourcc,
280 ✗ fs.stepwise.max_width, fs.stepwise.max_height, modes);
281 ✗ break;
282 }
283 }
284 ✗ }
285
286 ✗ close(fd);
287 ✗ return modes;
288 ✗ }
289
290 #else // !HAS_V4L2
291
292 bool ApplyControls(const std::string& device, const ControlSettings& s, std::string& summary) {
293 summary.clear();
294 return false;
295 }
296
297 std::vector<Mode> EnumerateModes(const std::string& device) {
298 return {};
299 }
300
301 #endif // HAS_V4L2
302
303 ✗ bool SelectMode(const std::vector<Mode>& modes, int width, int height, int fps, Mode& out) {
304 ✗ if (modes.empty())
305 ✗ return false;
306
307 // How good a mode is, ranked lexicographically most-significant first.
308 // Expressed as a tuple rather than a weighted score so the priorities
309 // stay literally the priorities: no arithmetic where a big enough
310 // difference in one term can quietly outvote the term above it.
311 struct Rank {
312 bool coversSize; // can satisfy the request by downscaling, not upscaling
313 long sizeFit; // closeness to the requested area, in the right direction
314 bool coversRate; // no framerate interpolation needed
315 long rateFit; // closeness to the requested rate, in the right direction
316
317 ✗ bool operator>(const Rank& o) const {
318 ✗ if (coversSize != o.coversSize)
319 ✗ return coversSize;
320 ✗ if (sizeFit != o.sizeFit)
321 ✗ return sizeFit > o.sizeFit;
322 ✗ if (coversRate != o.coversRate)
323 ✗ return coversRate;
324 ✗ return rateFit > o.rateFit;
325 }
326 };
327
328 ✗ const Mode* best = nullptr;
329 ✗ Rank bestRank{};
330
331 ✗ for (const auto& m : modes) {
332 ✗ if (m.width <= 0 || m.height <= 0)
333 ✗ continue;
334
335 // Exact match on all three wins outright -- nothing to resample.
336 ✗ if (m.width == width && m.height == height && m.fpsDen == 1 && m.fpsNum == fps) {
337 ✗ out = m;
338 ✗ return true;
339 }
340
341 ✗ const long area = (long)m.width * m.height;
342 Rank r;
343 ✗ r.coversSize = (m.width >= width && m.height >= height);
344 // Within the covering set the smallest such mode is the best fit
345 // (least wasted USB bandwidth, least downscaling); within the
346 // non-covering set the largest is. Both read as "closest to what
347 // was asked for", and the two sets are never compared on this term
348 // because coversSize is ranked above it.
349 ✗ r.sizeFit = r.coversSize ? -area : area;
350 ✗ r.coversRate = (m.fps() >= fps - 0.01);
351 // Same shape as sizeFit: among modes fast enough, the slowest is the
352 // best fit -- capturing 30fps to feed a 15fps output just burns USB
353 // bandwidth and CPU on frames videorate immediately discards. Among
354 // modes that are all too slow, the fastest is.
355 ✗ r.rateFit = r.coversRate ? -(long)(m.fps() * 100) : (long)(m.fps() * 100);
356
357 ✗ if (!best || r > bestRank) {
358 ✗ best = &m;
359 ✗ bestRank = r;
360 }
361 }
362
363 ✗ if (!best)
364 ✗ return false;
365
366 ✗ out = *best;
367 ✗ return true;
368 }
369
370 ✗ std::string ModeToCaps(const Mode& m) {
371 // Only fourccs whose GStreamer caps we are certain of. Guessing here
372 // would produce a capsfilter the device can never satisfy, which fails
373 // negotiation and leaves the pipeline stuck in READY -- the exact
374 // failure the unconstrained pipeline was written to avoid.
375 ✗ std::string media;
376 ✗ if (m.fourcc == "MJPG" || m.fourcc == "JPEG") {
377 ✗ media = "image/jpeg";
378 ✗ } else if (m.fourcc == "H264") {
379 ✗ media = "video/x-h264";
380 ✗ } else if (m.fourcc == "YUYV") {
381 ✗ media = "video/x-raw,format=YUY2";
382 ✗ } else if (m.fourcc == "UYVY") {
383 ✗ media = "video/x-raw,format=UYVY";
384 ✗ } else if (m.fourcc == "NV12") {
385 ✗ media = "video/x-raw,format=NV12";
386 ✗ } else if (m.fourcc == "YU12") {
387 ✗ media = "video/x-raw,format=I420";
388 ✗ } else if (m.fourcc == "YV12") {
389 ✗ media = "video/x-raw,format=YV12";
390 ✗ } else if (m.fourcc == "GREY") {
391 ✗ media = "video/x-raw,format=GRAY8";
392 ✗ } else if (m.fourcc == "RGB3") {
393 ✗ media = "video/x-raw,format=RGB";
394 ✗ } else if (m.fourcc == "BGR3") {
395 ✗ media = "video/x-raw,format=BGR";
396 } else {
397 ✗ return "";
398 }
399
400 ✗ std::string caps = media + ",width=" + std::to_string(m.width) + ",height=" + std::to_string(m.height);
401 ✗ if (m.fpsNum > 0 && m.fpsDen > 0) {
402 ✗ caps += ",framerate=" + std::to_string(m.fpsNum) + "/" + std::to_string(m.fpsDen);
403 }
404 ✗ return caps;
405 ✗ }
406
407 } // namespace V4L2Device
408