| 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 |