2016-09-16 17:57:17 -07:00
|
|
|
/*----------------------------------------------------------------------------*/
|
|
|
|
|
/* Copyright (c) FIRST 2016. All Rights Reserved. */
|
|
|
|
|
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
|
|
|
|
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
|
|
|
|
/* the project. */
|
|
|
|
|
/*----------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
|
|
#include "USBCameraImpl.h"
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
#ifdef __linux__
|
|
|
|
|
#include <dirent.h>
|
|
|
|
|
#include <fcntl.h>
|
2016-10-13 00:16:24 -07:00
|
|
|
#include <libgen.h>
|
2016-09-16 17:57:17 -07:00
|
|
|
#include <linux/kernel.h>
|
|
|
|
|
#include <linux/types.h>
|
|
|
|
|
#include <linux/videodev2.h>
|
2016-10-13 00:16:24 -07:00
|
|
|
#include <sys/eventfd.h>
|
|
|
|
|
#include <sys/inotify.h>
|
2016-09-16 17:57:17 -07:00
|
|
|
#include <sys/ioctl.h>
|
2016-10-13 00:16:24 -07:00
|
|
|
#include <sys/select.h>
|
2016-09-16 17:57:17 -07:00
|
|
|
#include <sys/stat.h>
|
2016-10-13 00:16:24 -07:00
|
|
|
#include <sys/time.h>
|
2016-09-16 17:57:17 -07:00
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
|
|
#elif defined(_WIN32)
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include "llvm/raw_ostream.h"
|
|
|
|
|
#include "llvm/SmallString.h"
|
2016-10-22 22:09:47 -07:00
|
|
|
#include "support/timestamp.h"
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-11-05 22:11:55 -07:00
|
|
|
#include "cscore_cpp.h"
|
2016-09-16 17:57:17 -07:00
|
|
|
#include "c_util.h"
|
|
|
|
|
#include "Handle.h"
|
|
|
|
|
#include "Log.h"
|
2016-11-18 08:49:20 -08:00
|
|
|
#include "Notifier.h"
|
2016-09-16 17:57:17 -07:00
|
|
|
|
|
|
|
|
using namespace cs;
|
|
|
|
|
|
|
|
|
|
#ifdef __linux__
|
|
|
|
|
|
2016-09-29 00:04:16 -07:00
|
|
|
// Conversions v4l2_fract time per frame from/to frames per second (fps)
|
|
|
|
|
static inline int FractToFPS(const struct v4l2_fract& timeperframe) {
|
|
|
|
|
return (1.0 * timeperframe.denominator) / timeperframe.numerator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline struct v4l2_fract FPSToFract(int fps) {
|
|
|
|
|
struct v4l2_fract timeperframe;
|
|
|
|
|
timeperframe.numerator = 1;
|
|
|
|
|
timeperframe.denominator = fps;
|
|
|
|
|
return timeperframe;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conversion from v4l2_format pixelformat to VideoMode::PixelFormat
|
|
|
|
|
static VideoMode::PixelFormat ToPixelFormat(__u32 pixelformat) {
|
|
|
|
|
switch (pixelformat) {
|
|
|
|
|
case V4L2_PIX_FMT_MJPEG:
|
|
|
|
|
return VideoMode::kMJPEG;
|
|
|
|
|
case V4L2_PIX_FMT_YUYV:
|
|
|
|
|
return VideoMode::kYUYV;
|
|
|
|
|
case V4L2_PIX_FMT_RGB565:
|
|
|
|
|
return VideoMode::kRGB565;
|
|
|
|
|
default:
|
|
|
|
|
return VideoMode::kUnknown;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-20 22:28:48 -07:00
|
|
|
// Removes non-alphanumeric characters and replaces spaces with underscores.
|
|
|
|
|
// e.g. "Zoom, Absolute" -> "zoom_absolute", "Pan (Absolute)" -> "pan_absolute"
|
|
|
|
|
static llvm::StringRef NormalizeName(llvm::StringRef name,
|
|
|
|
|
llvm::SmallVectorImpl<char>& buf) {
|
|
|
|
|
bool newWord = false;
|
|
|
|
|
for (auto ch : name) {
|
|
|
|
|
if (std::isalnum(ch)) {
|
|
|
|
|
if (newWord) buf.push_back('_');
|
|
|
|
|
newWord = false;
|
|
|
|
|
buf.push_back(std::tolower(ch));
|
|
|
|
|
} else if (!buf.empty())
|
|
|
|
|
newWord = true;
|
|
|
|
|
}
|
|
|
|
|
return llvm::StringRef(buf.data(), buf.size());
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 17:57:17 -07:00
|
|
|
#ifdef VIDIOC_QUERY_EXT_CTRL
|
|
|
|
|
USBCameraImpl::PropertyData::PropertyData(
|
|
|
|
|
const struct v4l2_query_ext_ctrl& ctrl)
|
2016-10-23 18:20:56 -07:00
|
|
|
: PropertyBase(llvm::StringRef{}, CS_PROP_NONE, ctrl.minimum, ctrl.maximum,
|
|
|
|
|
ctrl.step, ctrl.default_value, 0),
|
|
|
|
|
id(ctrl.id & V4L2_CTRL_ID_MASK),
|
|
|
|
|
type(ctrl.type) {
|
2016-11-15 23:54:50 -08:00
|
|
|
// propKind
|
2016-09-16 17:57:17 -07:00
|
|
|
switch (ctrl.type) {
|
|
|
|
|
case V4L2_CTRL_TYPE_INTEGER:
|
|
|
|
|
case V4L2_CTRL_TYPE_INTEGER64:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_INTEGER;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
case V4L2_CTRL_TYPE_BOOLEAN:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_BOOLEAN;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
case V4L2_CTRL_TYPE_INTEGER_MENU:
|
|
|
|
|
case V4L2_CTRL_TYPE_MENU:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_ENUM;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
case V4L2_CTRL_TYPE_STRING:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_STRING;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return; // others unsupported
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// name
|
|
|
|
|
std::size_t len = 0;
|
|
|
|
|
while (len < sizeof(ctrl.name) && ctrl.name[len] != '\0') ++len;
|
2016-09-20 22:28:48 -07:00
|
|
|
llvm::SmallString<64> name_buf;
|
|
|
|
|
name = NormalizeName(llvm::StringRef(ctrl.name, len), name_buf);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
USBCameraImpl::PropertyData::PropertyData(const struct v4l2_queryctrl& ctrl)
|
2016-10-23 18:20:56 -07:00
|
|
|
: PropertyBase(llvm::StringRef{}, CS_PROP_NONE, ctrl.minimum, ctrl.maximum,
|
|
|
|
|
ctrl.step, ctrl.default_value, 0),
|
|
|
|
|
id(ctrl.id & V4L2_CTRL_ID_MASK),
|
|
|
|
|
type(ctrl.type) {
|
2016-11-15 23:54:50 -08:00
|
|
|
// propKind
|
2016-09-16 17:57:17 -07:00
|
|
|
switch (ctrl.type) {
|
|
|
|
|
case V4L2_CTRL_TYPE_INTEGER:
|
|
|
|
|
case V4L2_CTRL_TYPE_INTEGER64:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_INTEGER;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
case V4L2_CTRL_TYPE_BOOLEAN:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_BOOLEAN;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
case V4L2_CTRL_TYPE_INTEGER_MENU:
|
|
|
|
|
case V4L2_CTRL_TYPE_MENU:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_ENUM;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
case V4L2_CTRL_TYPE_STRING:
|
2016-11-15 23:54:50 -08:00
|
|
|
propKind = CS_PROP_STRING;
|
2016-09-16 17:57:17 -07:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return; // others unsupported
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// name
|
|
|
|
|
std::size_t len = 0;
|
|
|
|
|
while (len < sizeof(ctrl.name) && ctrl.name[len] != '\0') ++len;
|
2016-09-20 22:28:48 -07:00
|
|
|
llvm::SmallString<64> name_buf;
|
|
|
|
|
name = NormalizeName(
|
|
|
|
|
llvm::StringRef(reinterpret_cast<const char*>(ctrl.name), len), name_buf);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline int CheckedIoctl(int fd, unsigned long req, void* data,
|
2016-10-13 00:16:24 -07:00
|
|
|
const char* name, const char* file, int line,
|
|
|
|
|
bool quiet) {
|
2016-09-16 17:57:17 -07:00
|
|
|
int retval = ioctl(fd, req, data);
|
2016-10-13 00:16:24 -07:00
|
|
|
if (!quiet && retval < 0) {
|
|
|
|
|
llvm::SmallString<64> localfile{file};
|
|
|
|
|
localfile.push_back('\0');
|
|
|
|
|
ERROR("ioctl " << name << " failed at " << basename(localfile.data()) << ":"
|
|
|
|
|
<< line << ": " << std::strerror(errno));
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
return retval;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
#define DoIoctl(fd, req, data) \
|
|
|
|
|
CheckedIoctl(fd, req, data, #req, __FILE__, __LINE__, false)
|
|
|
|
|
#define TryIoctl(fd, req, data) \
|
|
|
|
|
CheckedIoctl(fd, req, data, #req, __FILE__, __LINE__, true)
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-23 18:20:56 -07:00
|
|
|
static std::unique_ptr<USBCameraImpl::PropertyData> ExtCtrlIoctl(int fd,
|
|
|
|
|
__u32* id) {
|
2016-09-16 17:57:17 -07:00
|
|
|
int rc;
|
2016-10-23 18:20:56 -07:00
|
|
|
std::unique_ptr<USBCameraImpl::PropertyData> prop;
|
2016-09-16 17:57:17 -07:00
|
|
|
#ifdef VIDIOC_QUERY_EXT_CTRL
|
|
|
|
|
v4l2_query_ext_ctrl qc_ext;
|
|
|
|
|
std::memset(&qc_ext, 0, sizeof(qc_ext));
|
|
|
|
|
qc_ext.id = *id;
|
|
|
|
|
rc = TryIoctl(fd, VIDIOC_QUERY_EXT_CTRL, &qc_ext);
|
|
|
|
|
if (rc == 0) {
|
|
|
|
|
*id = qc_ext.id; // copy back
|
|
|
|
|
// We don't support array types
|
2016-10-23 18:20:56 -07:00
|
|
|
if (qc_ext.elems > 1 || qc_ext.nr_of_dims > 0) return nullptr;
|
|
|
|
|
prop = llvm::make_unique<USBCameraImpl::PropertyData>(qc_ext);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
#endif
|
2016-10-23 18:20:56 -07:00
|
|
|
if (!prop) {
|
2016-10-13 00:16:24 -07:00
|
|
|
// Fall back to normal QUERYCTRL
|
|
|
|
|
struct v4l2_queryctrl qc;
|
|
|
|
|
std::memset(&qc, 0, sizeof(qc));
|
|
|
|
|
qc.id = *id;
|
|
|
|
|
rc = TryIoctl(fd, VIDIOC_QUERYCTRL, &qc);
|
|
|
|
|
*id = qc.id; // copy back
|
2016-10-23 18:20:56 -07:00
|
|
|
if (rc != 0) return nullptr;
|
|
|
|
|
prop = llvm::make_unique<USBCameraImpl::PropertyData>(qc);
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache enum property choices
|
2016-11-15 23:54:50 -08:00
|
|
|
if (prop->propKind == CS_PROP_ENUM) {
|
2016-10-23 18:20:56 -07:00
|
|
|
prop->enumChoices.resize(prop->maximum + 1);
|
|
|
|
|
v4l2_querymenu qmenu;
|
|
|
|
|
std::memset(&qmenu, 0, sizeof(qmenu));
|
|
|
|
|
qmenu.id = *id;
|
|
|
|
|
for (int i = prop->minimum; i <= prop->maximum; ++i) {
|
|
|
|
|
qmenu.index = static_cast<__u32>(i);
|
|
|
|
|
if (TryIoctl(fd, VIDIOC_QUERYMENU, &qmenu) != 0) continue;
|
|
|
|
|
prop->enumChoices[i] = reinterpret_cast<const char*>(qmenu.name);
|
|
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-10-23 18:20:56 -07:00
|
|
|
|
|
|
|
|
return prop;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int GetIntCtrlIoctl(int fd, unsigned id, int type, int64_t* value) {
|
|
|
|
|
unsigned ctrl_class = V4L2_CTRL_ID2CLASS(id);
|
|
|
|
|
if (type == V4L2_CTRL_TYPE_INTEGER64 || V4L2_CTRL_DRIVER_PRIV(id) ||
|
|
|
|
|
(ctrl_class != V4L2_CTRL_CLASS_USER &&
|
|
|
|
|
ctrl_class != V4L2_CID_PRIVATE_BASE)) {
|
|
|
|
|
// Use extended control
|
|
|
|
|
struct v4l2_ext_control ctrl;
|
|
|
|
|
struct v4l2_ext_controls ctrls;
|
|
|
|
|
std::memset(&ctrl, 0, sizeof(ctrl));
|
2016-10-13 00:16:24 -07:00
|
|
|
std::memset(&ctrls, 0, sizeof(ctrls));
|
2016-09-16 17:57:17 -07:00
|
|
|
ctrl.id = id;
|
|
|
|
|
ctrls.ctrl_class = ctrl_class;
|
|
|
|
|
ctrls.count = 1;
|
|
|
|
|
ctrls.controls = &ctrl;
|
|
|
|
|
int rc = DoIoctl(fd, VIDIOC_G_EXT_CTRLS, &ctrls);
|
|
|
|
|
if (rc < 0) return rc;
|
|
|
|
|
*value = ctrl.value;
|
|
|
|
|
} else {
|
|
|
|
|
// Use normal control
|
|
|
|
|
struct v4l2_control ctrl;
|
2016-10-13 00:16:24 -07:00
|
|
|
std::memset(&ctrl, 0, sizeof(ctrl));
|
2016-09-16 17:57:17 -07:00
|
|
|
ctrl.id = id;
|
|
|
|
|
int rc = DoIoctl(fd, VIDIOC_G_CTRL, &ctrl);
|
|
|
|
|
if (rc < 0) return rc;
|
|
|
|
|
*value = ctrl.value;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int SetIntCtrlIoctl(int fd, unsigned id, int type, int64_t value) {
|
|
|
|
|
unsigned ctrl_class = V4L2_CTRL_ID2CLASS(id);
|
|
|
|
|
if (type == V4L2_CTRL_TYPE_INTEGER64 || V4L2_CTRL_DRIVER_PRIV(id) ||
|
|
|
|
|
(ctrl_class != V4L2_CTRL_CLASS_USER &&
|
|
|
|
|
ctrl_class != V4L2_CID_PRIVATE_BASE)) {
|
|
|
|
|
// Use extended control
|
|
|
|
|
struct v4l2_ext_control ctrl;
|
|
|
|
|
struct v4l2_ext_controls ctrls;
|
|
|
|
|
std::memset(&ctrl, 0, sizeof(ctrl));
|
2016-10-13 00:16:24 -07:00
|
|
|
std::memset(&ctrls, 0, sizeof(ctrls));
|
2016-09-16 17:57:17 -07:00
|
|
|
ctrl.id = id;
|
|
|
|
|
if (type == V4L2_CTRL_TYPE_INTEGER64)
|
|
|
|
|
ctrl.value64 = value;
|
|
|
|
|
else
|
|
|
|
|
ctrl.value = static_cast<__s32>(value);
|
|
|
|
|
ctrls.ctrl_class = ctrl_class;
|
|
|
|
|
ctrls.count = 1;
|
|
|
|
|
ctrls.controls = &ctrl;
|
|
|
|
|
return DoIoctl(fd, VIDIOC_S_EXT_CTRLS, &ctrls);
|
|
|
|
|
} else {
|
|
|
|
|
// Use normal control
|
|
|
|
|
struct v4l2_control ctrl;
|
|
|
|
|
ctrl.id = id;
|
|
|
|
|
ctrl.value = static_cast<__s32>(value);
|
|
|
|
|
return DoIoctl(fd, VIDIOC_S_CTRL, &ctrl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
static int GetStringCtrlIoctl(int fd, int id, int maximum,
|
|
|
|
|
std::string* value) {
|
|
|
|
|
struct v4l2_ext_control ctrl;
|
|
|
|
|
struct v4l2_ext_controls ctrls;
|
|
|
|
|
std::memset(&ctrl, 0, sizeof(ctrl));
|
|
|
|
|
std::memset(&ctrls, 0, sizeof(ctrls));
|
|
|
|
|
ctrl.id = id;
|
|
|
|
|
ctrls.ctrl_class = V4L2_CTRL_ID2CLASS(id);
|
|
|
|
|
ctrls.count = 1;
|
|
|
|
|
ctrls.controls = &ctrl;
|
|
|
|
|
int rc = DoIoctl(fd, VIDIOC_G_EXT_CTRLS, &ctrls);
|
|
|
|
|
if (rc < 0) {
|
|
|
|
|
value->clear();
|
|
|
|
|
return rc;
|
|
|
|
|
}
|
|
|
|
|
value->assign(ctrl.string, std::strlen(ctrl.string));
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int SetStringCtrlIoctl(int fd, int id, int maximum,
|
|
|
|
|
llvm::StringRef value) {
|
|
|
|
|
llvm::SmallString<64> str{value.substr(
|
|
|
|
|
0, std::min(value.size(), static_cast<std::size_t>(maximum)))};
|
|
|
|
|
|
|
|
|
|
struct v4l2_ext_control ctrl;
|
|
|
|
|
struct v4l2_ext_controls ctrls;
|
|
|
|
|
std::memset(&ctrl, 0, sizeof(ctrl));
|
|
|
|
|
std::memset(&ctrls, 0, sizeof(ctrls));
|
|
|
|
|
ctrl.id = id;
|
|
|
|
|
ctrl.size = str.size();
|
|
|
|
|
ctrl.string = const_cast<char*>(str.c_str());
|
|
|
|
|
ctrls.ctrl_class = V4L2_CTRL_ID2CLASS(id);
|
|
|
|
|
ctrls.count = 1;
|
|
|
|
|
ctrls.controls = &ctrl;
|
|
|
|
|
return DoIoctl(fd, VIDIOC_S_EXT_CTRLS, &ctrls);
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 17:57:17 -07:00
|
|
|
static std::string GetDescriptionImpl(const char* cpath) {
|
|
|
|
|
llvm::StringRef path{cpath};
|
|
|
|
|
int fd;
|
|
|
|
|
char pathBuf[128];
|
|
|
|
|
|
|
|
|
|
// If trying to get by id or path, follow symlink
|
|
|
|
|
if (path.startswith("/dev/v4l/by-id/")) {
|
|
|
|
|
ssize_t n = readlink(cpath, pathBuf, sizeof(pathBuf));
|
|
|
|
|
if (n > 0) path = llvm::StringRef(pathBuf, n);
|
|
|
|
|
} else if (path.startswith("/dev/v4l/by-path/")) {
|
|
|
|
|
ssize_t n = readlink(cpath, pathBuf, sizeof(pathBuf));
|
|
|
|
|
if (n > 0) path = llvm::StringRef(pathBuf, n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (path.startswith("/dev/video")) {
|
|
|
|
|
// Sometimes the /sys tree gives a better name.
|
|
|
|
|
llvm::SmallString<64> ifpath{"/sys/class/video4linux/"};
|
|
|
|
|
ifpath += path.substr(5);
|
|
|
|
|
ifpath += "/device/interface";
|
|
|
|
|
fd = open(ifpath.c_str(), O_RDONLY);
|
|
|
|
|
if (fd >= 0) {
|
|
|
|
|
char readBuf[128];
|
|
|
|
|
ssize_t n = read(fd, readBuf, sizeof(readBuf));
|
|
|
|
|
if (n > 0) {
|
|
|
|
|
close(fd);
|
|
|
|
|
return llvm::StringRef(readBuf, n).rtrim();
|
|
|
|
|
}
|
|
|
|
|
close(fd);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise use an ioctl to query the caps and get the card name
|
|
|
|
|
fd = open(cpath, O_RDWR);
|
|
|
|
|
if (fd >= 0) {
|
|
|
|
|
struct v4l2_capability vcap;
|
2016-10-13 00:16:24 -07:00
|
|
|
std::memset(&vcap, 0, sizeof(vcap));
|
2016-09-16 17:57:17 -07:00
|
|
|
if (DoIoctl(fd, VIDIOC_QUERYCAP, &vcap) >= 0) {
|
|
|
|
|
close(fd);
|
|
|
|
|
llvm::StringRef card{reinterpret_cast<const char*>(vcap.card)};
|
|
|
|
|
// try to convert "UVC Camera (0000:0000)" into a better name
|
|
|
|
|
int vendor = 0;
|
|
|
|
|
int product = 0;
|
|
|
|
|
if (card.startswith("UVC Camera (") &&
|
|
|
|
|
!card.substr(12, 4).getAsInteger(16, vendor) &&
|
|
|
|
|
!card.substr(17, 4).getAsInteger(16, product)) {
|
|
|
|
|
switch (vendor) {
|
|
|
|
|
case 0x046d:
|
|
|
|
|
switch (product) {
|
|
|
|
|
case 0x081b: return "Logitech, Inc. Webcam C310";
|
|
|
|
|
case 0x0825: return "Logitech, Inc. Webcam C270";
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
close(fd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::string{};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
USBCameraImpl::USBCameraImpl(llvm::StringRef name, llvm::StringRef path)
|
|
|
|
|
: SourceImpl{name},
|
|
|
|
|
m_path{path},
|
2016-10-13 00:16:24 -07:00
|
|
|
m_fd{-1},
|
|
|
|
|
m_command_fd{eventfd(0, 0)},
|
|
|
|
|
m_active{true} {
|
2016-10-23 18:20:56 -07:00
|
|
|
SetDescription(GetDescriptionImpl(m_path.c_str()));
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
USBCameraImpl::~USBCameraImpl() {
|
|
|
|
|
m_active = false;
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Just in case anyone is waiting...
|
|
|
|
|
m_responseCv.notify_all();
|
|
|
|
|
|
|
|
|
|
// Send message to wake up thread; select timeout will wake us up anyway,
|
|
|
|
|
// but this speeds shutdown.
|
|
|
|
|
Send(CreateMessage(Message::kNone));
|
|
|
|
|
|
|
|
|
|
// join camera thread
|
2016-11-18 09:20:52 -08:00
|
|
|
if (m_cameraThread.joinable()) m_cameraThread.join();
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
// close command fd
|
|
|
|
|
int fd = m_command_fd.exchange(-1);
|
|
|
|
|
if (fd >= 0) close(fd);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
static inline void DoFdSet(int fd, fd_set* set, int* nfds) {
|
|
|
|
|
if (fd >= 0) {
|
|
|
|
|
FD_SET(fd, set);
|
|
|
|
|
if ((fd + 1) > *nfds) *nfds = fd + 1;
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-11-18 09:20:52 -08:00
|
|
|
void USBCameraImpl::Start() {
|
|
|
|
|
// Kick off the camera thread
|
|
|
|
|
m_cameraThread = std::thread(&USBCameraImpl::CameraThreadMain, this);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::CameraThreadMain() {
|
|
|
|
|
// We want to be notified on file creation and deletion events in the device
|
|
|
|
|
// path. This is used to detect disconnects and reconnects.
|
|
|
|
|
std::unique_ptr<wpi::raw_fd_istream> notify_is;
|
|
|
|
|
int notify_fd = inotify_init();
|
|
|
|
|
if (notify_fd >= 0) {
|
|
|
|
|
// need to make a copy as dirname can modify it
|
|
|
|
|
llvm::SmallString<64> pathCopy{m_path};
|
|
|
|
|
pathCopy.push_back('\0');
|
|
|
|
|
if (inotify_add_watch(notify_fd, dirname(pathCopy.data()),
|
|
|
|
|
IN_CREATE | IN_DELETE) < 0) {
|
|
|
|
|
close(notify_fd);
|
|
|
|
|
notify_fd = -1;
|
|
|
|
|
} else {
|
|
|
|
|
notify_is.reset(new wpi::raw_fd_istream{
|
|
|
|
|
notify_fd, true, sizeof(struct inotify_event) + NAME_MAX + 1});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
bool notified = (notify_fd < 0); // treat as always notified if cannot notify
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Get the basename for later notify use
|
|
|
|
|
llvm::SmallString<64> pathCopy{m_path};
|
|
|
|
|
pathCopy.push_back('\0');
|
|
|
|
|
llvm::SmallString<64> base{basename(pathCopy.data())};
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Used to restart streaming on reconnect
|
|
|
|
|
bool wasStreaming = false;
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Default to not streaming
|
|
|
|
|
m_streaming = false;
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
while (m_active) {
|
|
|
|
|
// If not connected, try to reconnect
|
|
|
|
|
if (m_fd < 0) DeviceConnect();
|
|
|
|
|
|
|
|
|
|
// Make copies of fd's in case they go away
|
|
|
|
|
int command_fd = m_command_fd.load();
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (!m_active) break;
|
|
|
|
|
|
|
|
|
|
// Reset notified flag and restart streaming if necessary
|
|
|
|
|
if (fd >= 0) {
|
|
|
|
|
notified = (notify_fd < 0);
|
|
|
|
|
if (wasStreaming && !m_streaming) {
|
|
|
|
|
DeviceStreamOn();
|
|
|
|
|
wasStreaming = false;
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Turn off streaming if no one is listening, and turn it on if there is.
|
|
|
|
|
if (m_streaming && m_numSinksEnabled == 0) {
|
|
|
|
|
DeviceStreamOff();
|
|
|
|
|
} else if (!m_streaming && m_numSinksEnabled > 0) {
|
|
|
|
|
DeviceStreamOn();
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// The select timeout can be long unless we're trying to reconnect
|
|
|
|
|
struct timeval tv;
|
|
|
|
|
if (fd < 0 && notified) {
|
|
|
|
|
tv.tv_sec = 0;
|
|
|
|
|
tv.tv_usec = 300000;
|
|
|
|
|
} else {
|
|
|
|
|
tv.tv_sec = 2;
|
|
|
|
|
tv.tv_usec = 0;
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// select on applicable read descriptors
|
|
|
|
|
int nfds = 0;
|
|
|
|
|
fd_set readfds;
|
|
|
|
|
FD_ZERO(&readfds);
|
|
|
|
|
DoFdSet(command_fd, &readfds, &nfds);
|
|
|
|
|
if (m_streaming) DoFdSet(fd, &readfds, &nfds);
|
|
|
|
|
DoFdSet(notify_fd, &readfds, &nfds);
|
|
|
|
|
|
|
|
|
|
if (select(nfds, &readfds, nullptr, nullptr, &tv) < 0) {
|
|
|
|
|
ERROR("USB select(): " << strerror(errno));
|
|
|
|
|
break; // XXX: is this the right thing to do here?
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Double-check to see if we're shutting down
|
|
|
|
|
if (!m_active) break;
|
|
|
|
|
|
|
|
|
|
// Handle notify events
|
|
|
|
|
if (notify_fd >= 0 && FD_ISSET(notify_fd, &readfds)) {
|
|
|
|
|
DEBUG4("USB " << m_path << ": notify event");
|
|
|
|
|
struct inotify_event event;
|
|
|
|
|
do {
|
|
|
|
|
// Read the event structure
|
|
|
|
|
notify_is->read(&event, sizeof(event));
|
|
|
|
|
// Read the event name
|
|
|
|
|
llvm::SmallString<64> raw_name;
|
|
|
|
|
raw_name.resize(event.len);
|
|
|
|
|
notify_is->read(raw_name.data(), event.len);
|
|
|
|
|
// If the name is what we expect...
|
|
|
|
|
llvm::StringRef name{raw_name.c_str()};
|
|
|
|
|
DEBUG4("USB " << m_path << ": got event on '" << name << "' ("
|
|
|
|
|
<< name.size() << ") compare to '" << base << "' ("
|
|
|
|
|
<< base.size() << ") mask " << event.mask);
|
|
|
|
|
if (name == base) {
|
|
|
|
|
if ((event.mask & IN_DELETE) != 0) {
|
|
|
|
|
wasStreaming = m_streaming;
|
|
|
|
|
DeviceStreamOff();
|
|
|
|
|
DeviceDisconnect();
|
|
|
|
|
} else if ((event.mask & IN_CREATE) != 0) {
|
|
|
|
|
notified = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} while (!notify_is->has_error() &&
|
|
|
|
|
notify_is->in_avail() >= sizeof(event));
|
|
|
|
|
continue;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
// Handle commands
|
|
|
|
|
if (command_fd >= 0 && FD_ISSET(command_fd, &readfds)) {
|
|
|
|
|
DEBUG4("USB " << m_path << ": got command");
|
|
|
|
|
// Read it to clear
|
|
|
|
|
eventfd_t val;
|
|
|
|
|
eventfd_read(command_fd, &val);
|
|
|
|
|
DeviceProcessCommands();
|
|
|
|
|
continue;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Handle frames
|
|
|
|
|
if (m_streaming && fd >= 0 && FD_ISSET(fd, &readfds)) {
|
|
|
|
|
DEBUG4("USB " << m_path << ": grabbing image");
|
|
|
|
|
|
|
|
|
|
// Dequeue buffer
|
|
|
|
|
struct v4l2_buffer buf;
|
|
|
|
|
std::memset(&buf, 0, sizeof(buf));
|
|
|
|
|
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
buf.memory = V4L2_MEMORY_MMAP;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_DQBUF, &buf) != 0) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not dequeue buffer");
|
|
|
|
|
wasStreaming = m_streaming;
|
|
|
|
|
DeviceStreamOff();
|
|
|
|
|
DeviceDisconnect();
|
|
|
|
|
notified = true; // device wasn't deleted, just error'ed
|
|
|
|
|
continue; // will reconnect
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
if ((buf.flags & V4L2_BUF_FLAG_ERROR) == 0) {
|
|
|
|
|
DEBUG4("USB " << m_path << ": got image size=" << buf.bytesused
|
|
|
|
|
<< " index=" << buf.index);
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
if (buf.index >= kNumBuffers || !m_buffers[buf.index].m_data) {
|
|
|
|
|
WARNING("USB " << m_path << ": invalid buffer" << buf.index);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
PutFrame(static_cast<VideoMode::PixelFormat>(m_mode.pixelFormat),
|
2016-11-10 00:00:20 -08:00
|
|
|
m_mode.width, m_mode.height,
|
2016-10-13 00:16:24 -07:00
|
|
|
llvm::StringRef(
|
|
|
|
|
static_cast<const char*>(m_buffers[buf.index].m_data),
|
|
|
|
|
static_cast<std::size_t>(buf.bytesused)),
|
2016-10-22 22:09:47 -07:00
|
|
|
wpi::Now()); // TODO: time
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Requeue buffer
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_QBUF, &buf) != 0) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not requeue buffer");
|
|
|
|
|
wasStreaming = m_streaming;
|
|
|
|
|
DeviceStreamOff();
|
|
|
|
|
DeviceDisconnect();
|
|
|
|
|
notified = true; // device wasn't deleted, just error'ed
|
|
|
|
|
continue; // will reconnect
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// close camera connection
|
2016-10-13 00:32:06 -07:00
|
|
|
DeviceStreamOff();
|
2016-10-13 00:16:24 -07:00
|
|
|
DeviceDisconnect();
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::DeviceDisconnect() {
|
|
|
|
|
int fd = m_fd.exchange(-1);
|
|
|
|
|
if (fd < 0) return; // already disconnected
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Unmap buffers
|
|
|
|
|
for (int i = 0; i < kNumBuffers; ++i)
|
|
|
|
|
m_buffers[i] = std::move(USBCameraBuffer{});
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Close device
|
|
|
|
|
close(fd);
|
2016-11-18 09:20:52 -08:00
|
|
|
|
|
|
|
|
// Notify
|
|
|
|
|
Notifier::GetInstance().NotifySource(*this, CS_SOURCE_DISCONNECTED);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::DeviceConnect() {
|
|
|
|
|
if (m_fd >= 0) return;
|
|
|
|
|
|
|
|
|
|
INFO("Connecting to USB camera on " << m_path);
|
|
|
|
|
|
|
|
|
|
// Try to open the device
|
|
|
|
|
DEBUG3("USB " << m_path << ": opening device");
|
|
|
|
|
int fd = open(m_path.c_str(), O_RDWR);
|
|
|
|
|
if (fd < 0) return;
|
|
|
|
|
m_fd = fd;
|
|
|
|
|
|
|
|
|
|
// Get capabilities
|
|
|
|
|
DEBUG3("USB " << m_path << ": getting capabilities");
|
|
|
|
|
struct v4l2_capability vcap;
|
|
|
|
|
std::memset(&vcap, 0, sizeof(vcap));
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_QUERYCAP, &vcap) >= 0) {
|
|
|
|
|
m_capabilities = vcap.capabilities;
|
|
|
|
|
if (m_capabilities & V4L2_CAP_DEVICE_CAPS)
|
|
|
|
|
m_capabilities = vcap.device_caps;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Get or restore video mode
|
|
|
|
|
if (!m_properties_cached) {
|
|
|
|
|
DEBUG3("USB " << m_path << ": caching properties");
|
|
|
|
|
DeviceCacheProperties();
|
|
|
|
|
DeviceCacheVideoModes();
|
2016-10-15 17:24:47 -07:00
|
|
|
DeviceCacheMode();
|
2016-10-13 00:16:24 -07:00
|
|
|
m_properties_cached = true;
|
|
|
|
|
} else {
|
|
|
|
|
DEBUG3("USB " << m_path << ": restoring video mode");
|
2016-10-15 17:24:47 -07:00
|
|
|
DeviceSetMode();
|
|
|
|
|
DeviceSetFPS();
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Restore settings
|
|
|
|
|
DEBUG3("USB " << m_path << ": restoring settings");
|
|
|
|
|
std::unique_lock<std::mutex> lock2(m_mutex);
|
|
|
|
|
for (std::size_t i = 0; i < m_propertyData.size(); ++i) {
|
|
|
|
|
const auto& prop = m_propertyData[i];
|
2016-10-23 18:20:56 -07:00
|
|
|
if (!prop->valueSet) continue;
|
|
|
|
|
if (!DeviceSetProperty(lock2, static_cast<const PropertyData&>(*prop)))
|
|
|
|
|
WARNING("USB " << m_path << ": failed to set property " << prop->name);
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Request buffers
|
|
|
|
|
DEBUG3("USB " << m_path << ": allocating buffers");
|
|
|
|
|
struct v4l2_requestbuffers rb;
|
|
|
|
|
std::memset(&rb, 0, sizeof(rb));
|
|
|
|
|
rb.count = kNumBuffers;
|
|
|
|
|
rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
rb.memory = V4L2_MEMORY_MMAP;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_REQBUFS, &rb) != 0) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not allocate buffers");
|
|
|
|
|
close(fd);
|
|
|
|
|
m_fd = -1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map buffers
|
|
|
|
|
DEBUG3("USB " << m_path << ": mapping buffers");
|
|
|
|
|
for (int i = 0; i < kNumBuffers; ++i) {
|
|
|
|
|
struct v4l2_buffer buf;
|
|
|
|
|
std::memset(&buf, 0, sizeof(buf));
|
|
|
|
|
buf.index = i;
|
|
|
|
|
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
buf.memory = V4L2_MEMORY_MMAP;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_QUERYBUF, &buf) != 0) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not query buffer " << i);
|
|
|
|
|
close(fd);
|
|
|
|
|
m_fd = -1;
|
2016-09-16 17:57:17 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
DEBUG4("USB " << m_path << ": buf " << i << " length=" << buf.length
|
|
|
|
|
<< " offset=" << buf.m.offset);
|
|
|
|
|
|
|
|
|
|
m_buffers[i] = std::move(USBCameraBuffer(fd, buf.length, buf.m.offset));
|
|
|
|
|
if (!m_buffers[i].m_data) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not map buffer " << i);
|
|
|
|
|
// release other buffers
|
|
|
|
|
for (int j = 0; j < i; ++j) m_buffers[j] = std::move(USBCameraBuffer{});
|
|
|
|
|
close(fd);
|
|
|
|
|
m_fd = -1;
|
2016-09-16 17:57:17 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
DEBUG4("USB " << m_path << ": buf " << i
|
|
|
|
|
<< " address=" << m_buffers[i].m_data);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
2016-11-18 09:20:52 -08:00
|
|
|
|
|
|
|
|
// Notify
|
|
|
|
|
Notifier::GetInstance().NotifySource(*this, CS_SOURCE_CONNECTED);
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
bool USBCameraImpl::DeviceStreamOn() {
|
|
|
|
|
if (m_streaming) return false; // ignore if already enabled
|
2016-09-16 17:57:17 -07:00
|
|
|
int fd = m_fd.load();
|
2016-10-13 00:16:24 -07:00
|
|
|
if (fd < 0) return false;
|
|
|
|
|
|
|
|
|
|
// Queue buffers
|
|
|
|
|
DEBUG3("USB " << m_path << ": queuing buffers");
|
|
|
|
|
for (int i = 0; i < kNumBuffers; ++i) {
|
|
|
|
|
struct v4l2_buffer buf;
|
|
|
|
|
std::memset(&buf, 0, sizeof(buf));
|
|
|
|
|
buf.index = i;
|
|
|
|
|
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
buf.memory = V4L2_MEMORY_MMAP;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_QBUF, &buf) != 0) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not queue buffer " << i);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Turn stream on
|
|
|
|
|
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_STREAMON, &type) != 0) return false;
|
|
|
|
|
DEBUG4("USB " << m_path << ": enabled streaming");
|
|
|
|
|
m_streaming = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
bool USBCameraImpl::DeviceStreamOff() {
|
|
|
|
|
if (!m_streaming) return false; // ignore if already disabled
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (fd < 0) return false;
|
|
|
|
|
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_STREAMOFF, &type) != 0) return false;
|
|
|
|
|
DEBUG4("USB " << m_path << ": disabled streaming");
|
|
|
|
|
m_streaming = false;
|
|
|
|
|
return true;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::DeviceProcessCommands() {
|
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
|
if (m_commands.empty()) return;
|
|
|
|
|
while (!m_commands.empty()) {
|
|
|
|
|
auto msg = std::move(m_commands.back());
|
|
|
|
|
m_commands.pop_back();
|
|
|
|
|
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kCmdSetMode ||
|
|
|
|
|
msg->kind == Message::kCmdSetPixelFormat ||
|
|
|
|
|
msg->kind == Message::kCmdSetResolution ||
|
|
|
|
|
msg->kind == Message::kCmdSetFPS) {
|
2016-10-13 00:16:24 -07:00
|
|
|
VideoMode newMode;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kCmdSetMode) {
|
2016-10-13 00:16:24 -07:00
|
|
|
newMode.pixelFormat = msg->data[0];
|
|
|
|
|
newMode.width = msg->data[1];
|
|
|
|
|
newMode.height = msg->data[2];
|
|
|
|
|
newMode.fps = msg->data[3];
|
|
|
|
|
m_modeSetPixelFormat = true;
|
|
|
|
|
m_modeSetResolution = true;
|
|
|
|
|
m_modeSetFPS = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
} else if (msg->kind == Message::kCmdSetPixelFormat) {
|
2016-10-13 00:16:24 -07:00
|
|
|
newMode = m_mode;
|
|
|
|
|
newMode.pixelFormat = msg->data[0];
|
|
|
|
|
m_modeSetPixelFormat = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
} else if (msg->kind == Message::kCmdSetResolution) {
|
2016-10-13 00:16:24 -07:00
|
|
|
newMode = m_mode;
|
|
|
|
|
newMode.width = msg->data[0];
|
|
|
|
|
newMode.height = msg->data[1];
|
|
|
|
|
m_modeSetResolution = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
} else if (msg->kind == Message::kCmdSetFPS) {
|
2016-10-13 00:16:24 -07:00
|
|
|
newMode = m_mode;
|
|
|
|
|
newMode.fps = msg->data[0];
|
|
|
|
|
m_modeSetFPS = true;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-15 17:24:47 -07:00
|
|
|
// If the pixel format or resolution changed, we need to disconnect and
|
|
|
|
|
// reconnect
|
|
|
|
|
if (newMode.pixelFormat != m_mode.pixelFormat ||
|
|
|
|
|
newMode.width != m_mode.width || newMode.height != m_mode.height) {
|
|
|
|
|
m_mode = newMode;
|
2016-10-13 00:16:24 -07:00
|
|
|
lock.unlock();
|
|
|
|
|
bool wasStreaming = m_streaming;
|
|
|
|
|
if (wasStreaming) DeviceStreamOff();
|
|
|
|
|
if (m_fd >= 0) {
|
|
|
|
|
DeviceDisconnect();
|
|
|
|
|
DeviceConnect();
|
|
|
|
|
}
|
|
|
|
|
if (wasStreaming) DeviceStreamOn();
|
2016-11-18 09:20:52 -08:00
|
|
|
Notifier::GetInstance().NotifySource(*this,
|
|
|
|
|
CS_SOURCE_VIDEOMODE_CHANGED);
|
2016-10-13 00:16:24 -07:00
|
|
|
lock.lock();
|
2016-10-15 17:24:47 -07:00
|
|
|
} else if (newMode.fps != m_mode.fps) {
|
2016-10-13 00:16:24 -07:00
|
|
|
m_mode = newMode;
|
2016-10-15 17:24:47 -07:00
|
|
|
lock.unlock();
|
|
|
|
|
DeviceSetFPS();
|
2016-11-18 09:20:52 -08:00
|
|
|
Notifier::GetInstance().NotifySource(*this,
|
|
|
|
|
CS_SOURCE_VIDEOMODE_CHANGED);
|
2016-10-15 17:24:47 -07:00
|
|
|
lock.lock();
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-11-15 23:54:50 -08:00
|
|
|
} else if (msg->kind == Message::kCmdSetProperty ||
|
|
|
|
|
msg->kind == Message::kCmdSetPropertyStr) {
|
2016-10-13 00:16:24 -07:00
|
|
|
int property = msg->data[0];
|
|
|
|
|
|
|
|
|
|
// Look up
|
2016-10-23 18:20:56 -07:00
|
|
|
auto prop = static_cast<PropertyData*>(GetProperty(property));
|
2016-10-13 00:16:24 -07:00
|
|
|
if (!prop) {
|
2016-11-15 23:54:50 -08:00
|
|
|
msg->kind = Message::kError;
|
2016-10-13 00:16:24 -07:00
|
|
|
msg->data[0] = CS_INVALID_PROPERTY;
|
|
|
|
|
goto done;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-15 23:54:50 -08:00
|
|
|
// Check kind match
|
|
|
|
|
if ((msg->kind == Message::kCmdSetPropertyStr &&
|
|
|
|
|
prop->propKind != CS_PROP_STRING) ||
|
|
|
|
|
(msg->kind == Message::kCmdSetProperty &&
|
|
|
|
|
(prop->propKind &
|
2016-10-13 00:16:24 -07:00
|
|
|
(CS_PROP_BOOLEAN | CS_PROP_INTEGER | CS_PROP_ENUM)) == 0)) {
|
2016-11-15 23:54:50 -08:00
|
|
|
msg->kind = Message::kError;
|
2016-10-13 00:16:24 -07:00
|
|
|
msg->data[0] = CS_WRONG_PROPERTY_TYPE;
|
|
|
|
|
goto done;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we're connected...
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (fd >= 0) {
|
|
|
|
|
// Get into local variables before we release the lock
|
|
|
|
|
int rv;
|
|
|
|
|
unsigned id = prop->id;
|
|
|
|
|
int maximum = prop->maximum;
|
|
|
|
|
int type = prop->type;
|
|
|
|
|
|
|
|
|
|
// Set the property value on the device
|
|
|
|
|
lock.unlock();
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kCmdSetPropertyStr)
|
2016-10-13 00:16:24 -07:00
|
|
|
rv = SetStringCtrlIoctl(fd, id, maximum, msg->dataStr);
|
|
|
|
|
else
|
|
|
|
|
rv =
|
|
|
|
|
SetIntCtrlIoctl(fd, id, type, static_cast<int64_t>(msg->data[1]));
|
|
|
|
|
lock.lock();
|
|
|
|
|
|
|
|
|
|
if (rv < 0) {
|
2016-11-15 23:54:50 -08:00
|
|
|
msg->kind = Message::kError;
|
2016-10-13 00:16:24 -07:00
|
|
|
msg->data[0] = CS_PROPERTY_WRITE_FAILED;
|
|
|
|
|
goto done;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache the set value. We need to re-get the pointer due to
|
|
|
|
|
// releasing the lock.
|
2016-10-23 18:20:56 -07:00
|
|
|
prop = static_cast<PropertyData*>(GetProperty(property));
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kCmdSetPropertyStr)
|
2016-10-13 00:16:24 -07:00
|
|
|
prop->valueStr = msg->dataStr;
|
|
|
|
|
else
|
|
|
|
|
prop->value = msg->data[1];
|
|
|
|
|
prop->valueSet = true;
|
2016-11-18 10:08:24 -08:00
|
|
|
// Only notify updates after we've notified created
|
|
|
|
|
if (m_properties_cached)
|
|
|
|
|
Notifier::GetInstance().NotifySourceProperty(
|
|
|
|
|
*this, CS_SOURCE_PROPERTY_VALUE_UPDATED, property, prop->propKind,
|
|
|
|
|
prop->value, prop->valueStr);
|
2016-11-15 23:54:50 -08:00
|
|
|
msg->kind = Message::kOk;
|
|
|
|
|
} else if (msg->kind == Message::kNumSinksChanged ||
|
|
|
|
|
msg->kind == Message::kNumSinksEnabledChanged) {
|
2016-10-13 00:16:24 -07:00
|
|
|
// These are send-only messages, so recycle here. DestroyMessage needs
|
|
|
|
|
// the mutex, so unlock it. We don't actually do anything directly
|
|
|
|
|
// based on these messages (instead we check in the main loop), but
|
|
|
|
|
// they do wake up the thread.
|
|
|
|
|
lock.unlock();
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
lock.lock();
|
|
|
|
|
} else {
|
2016-11-15 23:54:50 -08:00
|
|
|
msg->kind = Message::kNone;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
done:
|
|
|
|
|
if (msg) m_responses.emplace_back(std::move(msg));
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
lock.unlock();
|
|
|
|
|
m_responseCv.notify_all();
|
|
|
|
|
}
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-15 17:24:47 -07:00
|
|
|
void USBCameraImpl::DeviceSetMode() {
|
2016-09-16 17:57:17 -07:00
|
|
|
int fd = m_fd.load();
|
2016-10-13 00:16:24 -07:00
|
|
|
if (fd < 0) return;
|
2016-09-16 17:57:17 -07:00
|
|
|
|
2016-10-15 17:24:47 -07:00
|
|
|
struct v4l2_format vfmt;
|
|
|
|
|
std::memset(&vfmt, 0, sizeof(vfmt));
|
|
|
|
|
#ifdef V4L2_CAP_EXT_PIX_FORMAT
|
|
|
|
|
vfmt.fmt.pix.priv = (m_capabilities & V4L2_CAP_EXT_PIX_FORMAT) != 0
|
|
|
|
|
? V4L2_PIX_FMT_PRIV_MAGIC
|
|
|
|
|
: 0;
|
|
|
|
|
#endif
|
|
|
|
|
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
switch (m_mode.pixelFormat) {
|
|
|
|
|
case VideoMode::kMJPEG:
|
|
|
|
|
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kYUYV:
|
|
|
|
|
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
|
|
|
|
|
break;
|
|
|
|
|
case VideoMode::kRGB565:
|
|
|
|
|
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
WARNING("USB " << m_path << ": could not set format "
|
|
|
|
|
<< m_mode.pixelFormat << ", defaulting to MJPEG");
|
|
|
|
|
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
|
|
|
|
|
break;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
2016-10-15 17:24:47 -07:00
|
|
|
vfmt.fmt.pix.width = m_mode.width;
|
|
|
|
|
vfmt.fmt.pix.height = m_mode.height;
|
|
|
|
|
vfmt.fmt.pix.field = V4L2_FIELD_ANY;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_S_FMT, &vfmt) != 0) {
|
|
|
|
|
WARNING("USB " << m_path << ": could not set format "
|
|
|
|
|
<< m_mode.pixelFormat << " res " << m_mode.width << "x"
|
|
|
|
|
<< m_mode.height);
|
2016-10-15 17:30:28 -07:00
|
|
|
} else {
|
|
|
|
|
INFO("USB " << m_path << ": set format " << m_mode.pixelFormat << " res "
|
|
|
|
|
<< m_mode.width << "x" << m_mode.height);
|
2016-10-15 17:24:47 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void USBCameraImpl::DeviceSetFPS() {
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (fd < 0) return;
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
struct v4l2_streamparm parm;
|
|
|
|
|
std::memset(&parm, 0, sizeof(parm));
|
|
|
|
|
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_G_PARM, &parm) != 0) return;
|
|
|
|
|
if ((parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME) == 0) return;
|
|
|
|
|
std::memset(&parm, 0, sizeof(parm));
|
|
|
|
|
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
2016-10-15 17:24:47 -07:00
|
|
|
parm.parm.capture.timeperframe = FPSToFract(m_mode.fps);
|
2016-10-15 17:30:28 -07:00
|
|
|
if (DoIoctl(fd, VIDIOC_S_PARM, &parm) != 0)
|
|
|
|
|
WARNING("USB " << m_path << ": could not set FPS to " << m_mode.fps);
|
|
|
|
|
else
|
|
|
|
|
INFO("USB " << m_path << ": set FPS to " << m_mode.fps);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::DeviceCacheMode() {
|
2016-09-29 00:04:16 -07:00
|
|
|
int fd = m_fd.load();
|
2016-10-13 00:16:24 -07:00
|
|
|
if (fd < 0) return;
|
2016-09-29 00:04:16 -07:00
|
|
|
|
|
|
|
|
// Get format
|
|
|
|
|
struct v4l2_format vfmt;
|
|
|
|
|
std::memset(&vfmt, 0, sizeof(vfmt));
|
|
|
|
|
#ifdef V4L2_CAP_EXT_PIX_FORMAT
|
|
|
|
|
vfmt.fmt.pix.priv = (m_capabilities & V4L2_CAP_EXT_PIX_FORMAT) != 0
|
|
|
|
|
? V4L2_PIX_FMT_PRIV_MAGIC
|
|
|
|
|
: 0;
|
|
|
|
|
#endif
|
|
|
|
|
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
if (DoIoctl(fd, VIDIOC_G_FMT, &vfmt) != 0) {
|
2016-10-13 00:16:24 -07:00
|
|
|
ERROR("USB " << m_path << ": could not read current video mode");
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
|
|
|
m_mode = VideoMode{VideoMode::kMJPEG, 320, 240, 30};
|
|
|
|
|
return;
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
VideoMode::PixelFormat pixelFormat;
|
|
|
|
|
switch (vfmt.fmt.pix.pixelformat) {
|
|
|
|
|
case V4L2_PIX_FMT_MJPEG:
|
|
|
|
|
pixelFormat = VideoMode::kMJPEG;
|
|
|
|
|
break;
|
|
|
|
|
case V4L2_PIX_FMT_YUYV:
|
|
|
|
|
pixelFormat = VideoMode::kYUYV;
|
|
|
|
|
break;
|
|
|
|
|
case V4L2_PIX_FMT_RGB565:
|
|
|
|
|
pixelFormat = VideoMode::kRGB565;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
pixelFormat = VideoMode::kUnknown;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2016-10-15 17:24:47 -07:00
|
|
|
int width = vfmt.fmt.pix.width;
|
|
|
|
|
int height = vfmt.fmt.pix.height;
|
|
|
|
|
|
2016-09-29 00:04:16 -07:00
|
|
|
// Get FPS
|
|
|
|
|
int fps = 0;
|
|
|
|
|
struct v4l2_streamparm parm;
|
|
|
|
|
std::memset(&parm, 0, sizeof(parm));
|
|
|
|
|
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
if (TryIoctl(fd, VIDIOC_G_PARM, &parm) == 0) {
|
|
|
|
|
if (parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME)
|
|
|
|
|
fps = FractToFPS(parm.parm.capture.timeperframe);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-15 17:30:28 -07:00
|
|
|
// Update format with user changes.
|
|
|
|
|
bool formatChanged = false;
|
|
|
|
|
|
|
|
|
|
if (m_modeSetPixelFormat) {
|
|
|
|
|
// User set pixel format
|
|
|
|
|
if (pixelFormat != m_mode.pixelFormat) {
|
|
|
|
|
formatChanged = true;
|
|
|
|
|
pixelFormat = static_cast<VideoMode::PixelFormat>(m_mode.pixelFormat);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Default to MJPEG
|
|
|
|
|
if (pixelFormat != VideoMode::kMJPEG) {
|
|
|
|
|
formatChanged = true;
|
|
|
|
|
pixelFormat = VideoMode::kMJPEG;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_modeSetResolution) {
|
|
|
|
|
// User set resolution
|
|
|
|
|
if (width != m_mode.width || height != m_mode.height) {
|
|
|
|
|
formatChanged = true;
|
|
|
|
|
width = m_mode.width;
|
|
|
|
|
height = m_mode.height;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Default to lowest known resolution (based on number of total pixels)
|
|
|
|
|
int numPixels = width * height;
|
|
|
|
|
for (const auto& mode : m_videoModes) {
|
|
|
|
|
if (mode.pixelFormat != pixelFormat) continue;
|
|
|
|
|
int numPixelsHere = mode.width * mode.height;
|
|
|
|
|
if (numPixelsHere < numPixels) {
|
|
|
|
|
formatChanged = true;
|
|
|
|
|
numPixels = numPixelsHere;
|
|
|
|
|
width = mode.width;
|
|
|
|
|
height = mode.height;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-15 17:24:47 -07:00
|
|
|
// Update FPS with user changes
|
|
|
|
|
bool fpsChanged = false;
|
|
|
|
|
if (m_modeSetFPS && fps != m_mode.fps) {
|
|
|
|
|
fpsChanged = true;
|
|
|
|
|
fps = m_mode.fps;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save to global mode
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
|
|
|
m_mode.pixelFormat = pixelFormat;
|
|
|
|
|
m_mode.width = width;
|
|
|
|
|
m_mode.height = height;
|
|
|
|
|
m_mode.fps = fps;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (formatChanged) DeviceSetMode();
|
|
|
|
|
if (fpsChanged) DeviceSetFPS();
|
2016-11-18 09:20:52 -08:00
|
|
|
|
|
|
|
|
Notifier::GetInstance().NotifySource(*this, CS_SOURCE_VIDEOMODE_CHANGED);
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-23 18:20:56 -07:00
|
|
|
void USBCameraImpl::DeviceCacheProperty(std::unique_ptr<PropertyData> prop) {
|
2016-10-13 00:16:24 -07:00
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
2016-10-23 18:20:56 -07:00
|
|
|
int& ndx = m_properties[prop->name];
|
2016-10-13 00:16:24 -07:00
|
|
|
if (ndx == 0) {
|
|
|
|
|
// get the value
|
|
|
|
|
lock.unlock();
|
2016-10-23 18:20:56 -07:00
|
|
|
if (!DeviceGetProperty(prop.get()))
|
|
|
|
|
WARNING("USB " << m_path << ": failed to get property " << prop->name);
|
2016-10-13 00:16:24 -07:00
|
|
|
lock.lock();
|
|
|
|
|
// create a new index
|
|
|
|
|
ndx = m_propertyData.size() + 1;
|
|
|
|
|
m_propertyData.emplace_back(std::move(prop));
|
|
|
|
|
} else {
|
|
|
|
|
// merge with existing settings
|
2016-10-23 18:20:56 -07:00
|
|
|
auto prop2 = static_cast<PropertyData*>(GetProperty(ndx));
|
|
|
|
|
prop->valueSet = prop2->valueSet;
|
|
|
|
|
prop->value = prop2->value;
|
|
|
|
|
prop->valueStr = std::move(prop2->valueStr);
|
2016-10-13 00:16:24 -07:00
|
|
|
lock.unlock();
|
2016-10-23 18:20:56 -07:00
|
|
|
if (prop->valueSet) {
|
2016-10-13 00:16:24 -07:00
|
|
|
// set the value if it was previously set
|
2016-10-23 18:20:56 -07:00
|
|
|
if (!DeviceSetProperty(lock, *prop))
|
|
|
|
|
WARNING("USB " << m_path << ": failed to set property " << prop->name);
|
2016-10-13 00:16:24 -07:00
|
|
|
} else {
|
|
|
|
|
// otherwise get the value
|
2016-10-23 18:20:56 -07:00
|
|
|
if (!DeviceGetProperty(prop.get()))
|
|
|
|
|
WARNING("USB " << m_path << ": failed to get property " << prop->name);
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
|
|
|
|
lock.lock();
|
2016-10-23 18:20:56 -07:00
|
|
|
m_propertyData[ndx - 1] = std::move(prop);
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
2016-11-18 10:08:24 -08:00
|
|
|
auto eventProp = static_cast<PropertyData*>(GetProperty(ndx));
|
|
|
|
|
auto& notifier = Notifier::GetInstance();
|
|
|
|
|
notifier.NotifySourceProperty(*this, CS_SOURCE_PROPERTY_CREATED, ndx,
|
|
|
|
|
eventProp->propKind, eventProp->value,
|
|
|
|
|
eventProp->valueStr);
|
|
|
|
|
// also notify choices updated event for enum types
|
|
|
|
|
if (eventProp->propKind == CS_PROP_ENUM)
|
|
|
|
|
notifier.NotifySourceProperty(*this, CS_SOURCE_PROPERTY_CHOICES_UPDATED,
|
|
|
|
|
ndx, eventProp->propKind, eventProp->value,
|
|
|
|
|
llvm::StringRef{});
|
2016-10-13 00:16:24 -07:00
|
|
|
}
|
2016-09-29 00:04:16 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::DeviceCacheProperties() {
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (fd < 0) return;
|
|
|
|
|
|
|
|
|
|
constexpr __u32 nextFlags = V4L2_CTRL_FLAG_NEXT_CTRL
|
|
|
|
|
#ifdef V4L2_CTRL_FLAG_NEXT_COMPOUND
|
|
|
|
|
| V4L2_CTRL_FLAG_NEXT_COMPOUND
|
2016-09-29 00:04:16 -07:00
|
|
|
#endif
|
2016-10-13 00:16:24 -07:00
|
|
|
;
|
|
|
|
|
__u32 id = nextFlags;
|
2016-09-29 00:04:16 -07:00
|
|
|
|
2016-10-23 18:20:56 -07:00
|
|
|
while (auto prop = ExtCtrlIoctl(fd, &id)) {
|
2016-10-13 00:16:24 -07:00
|
|
|
DeviceCacheProperty(std::move(prop));
|
|
|
|
|
id |= nextFlags;
|
|
|
|
|
}
|
2016-09-29 00:04:16 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
if (id == nextFlags) {
|
|
|
|
|
// try just enumerating standard...
|
|
|
|
|
for (id = V4L2_CID_BASE; id < V4L2_CID_LASTP1; ++id) {
|
2016-10-23 18:20:56 -07:00
|
|
|
if (auto prop = ExtCtrlIoctl(fd, &id))
|
2016-10-13 00:16:24 -07:00
|
|
|
DeviceCacheProperty(std::move(prop));
|
|
|
|
|
}
|
|
|
|
|
// ... and custom controls
|
2016-10-23 18:20:56 -07:00
|
|
|
std::unique_ptr<PropertyData> prop;
|
|
|
|
|
for (id = V4L2_CID_PRIVATE_BASE; (prop = ExtCtrlIoctl(fd, &id)); ++id)
|
2016-10-13 00:16:24 -07:00
|
|
|
DeviceCacheProperty(std::move(prop));
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
void USBCameraImpl::DeviceCacheVideoModes() {
|
2016-09-29 00:04:16 -07:00
|
|
|
int fd = m_fd.load();
|
2016-10-13 00:16:24 -07:00
|
|
|
if (fd < 0) return;
|
2016-09-29 00:04:16 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
std::vector<VideoMode> modes;
|
2016-09-29 00:04:16 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Pixel formats
|
|
|
|
|
struct v4l2_fmtdesc fmt;
|
|
|
|
|
std::memset(&fmt, 0, sizeof(fmt));
|
2016-09-29 00:04:16 -07:00
|
|
|
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
|
|
|
for (fmt.index = 0; TryIoctl(fd, VIDIOC_ENUM_FMT, &fmt) >= 0; ++fmt.index) {
|
|
|
|
|
VideoMode::PixelFormat pixelFormat = ToPixelFormat(fmt.pixelformat);
|
|
|
|
|
if (pixelFormat == VideoMode::kUnknown) continue;
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Frame sizes
|
2016-09-29 00:04:16 -07:00
|
|
|
struct v4l2_frmsizeenum frmsize;
|
2016-10-13 00:16:24 -07:00
|
|
|
std::memset(&frmsize, 0, sizeof(frmsize));
|
2016-09-29 00:04:16 -07:00
|
|
|
frmsize.pixel_format = fmt.pixelformat;
|
|
|
|
|
for (frmsize.index = 0; TryIoctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) >= 0;
|
|
|
|
|
++frmsize.index) {
|
|
|
|
|
if (frmsize.type != V4L2_FRMSIZE_TYPE_DISCRETE) continue;
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
// Frame intervals
|
2016-09-29 00:04:16 -07:00
|
|
|
struct v4l2_frmivalenum frmival;
|
2016-10-13 00:16:24 -07:00
|
|
|
std::memset(&frmival, 0, sizeof(frmival));
|
2016-09-29 00:04:16 -07:00
|
|
|
frmival.pixel_format = fmt.pixelformat;
|
|
|
|
|
frmival.width = frmsize.discrete.width;
|
|
|
|
|
frmival.height = frmsize.discrete.height;
|
|
|
|
|
for (frmival.index = 0;
|
|
|
|
|
TryIoctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival) >= 0;
|
|
|
|
|
++frmival.index) {
|
|
|
|
|
if (frmival.type != V4L2_FRMIVAL_TYPE_DISCRETE) continue;
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
modes.emplace_back(pixelFormat,
|
|
|
|
|
static_cast<int>(frmsize.discrete.width),
|
|
|
|
|
static_cast<int>(frmsize.discrete.height),
|
|
|
|
|
FractToFPS(frmival.discrete));
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-18 09:20:52 -08:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
|
|
|
m_videoModes.swap(modes);
|
|
|
|
|
}
|
|
|
|
|
Notifier::GetInstance().NotifySource(*this, CS_SOURCE_VIDEOMODES_UPDATED);
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
bool USBCameraImpl::DeviceGetProperty(PropertyData* prop) {
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (fd < 0) return true;
|
|
|
|
|
int rv = 0;
|
|
|
|
|
|
2016-11-15 23:54:50 -08:00
|
|
|
switch (prop->propKind) {
|
2016-10-13 00:16:24 -07:00
|
|
|
case CS_PROP_BOOLEAN:
|
|
|
|
|
case CS_PROP_INTEGER:
|
|
|
|
|
case CS_PROP_ENUM:
|
|
|
|
|
{
|
|
|
|
|
int64_t value = 0;
|
|
|
|
|
rv = GetIntCtrlIoctl(fd, prop->id, prop->type, &value);
|
|
|
|
|
if (rv >= 0) prop->value = value;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case CS_PROP_STRING:
|
|
|
|
|
rv = GetStringCtrlIoctl(fd, prop->id, prop->maximum, &prop->valueStr);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
return rv >= 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool USBCameraImpl::DeviceSetProperty(std::unique_lock<std::mutex>& lock,
|
|
|
|
|
const PropertyData& prop) {
|
|
|
|
|
int fd = m_fd.load();
|
|
|
|
|
if (fd < 0) return true;
|
|
|
|
|
unsigned id = prop.id;
|
|
|
|
|
int rv = 0;
|
|
|
|
|
|
2016-11-15 23:54:50 -08:00
|
|
|
switch (prop.propKind) {
|
2016-10-13 00:16:24 -07:00
|
|
|
case CS_PROP_BOOLEAN:
|
|
|
|
|
case CS_PROP_INTEGER:
|
|
|
|
|
case CS_PROP_ENUM:
|
|
|
|
|
{
|
|
|
|
|
int type = prop.type;
|
|
|
|
|
int value = prop.value;
|
|
|
|
|
lock.unlock();
|
|
|
|
|
rv = SetIntCtrlIoctl(fd, id, type, value);
|
|
|
|
|
lock.lock();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case CS_PROP_STRING:
|
|
|
|
|
{
|
|
|
|
|
int maximum = prop.maximum;
|
|
|
|
|
llvm::SmallString<128> valueStr{prop.valueStr};
|
|
|
|
|
lock.unlock();
|
|
|
|
|
rv = SetStringCtrlIoctl(fd, id, maximum, valueStr);
|
|
|
|
|
lock.lock();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
return rv >= 0;
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
std::unique_ptr<USBCameraImpl::Message> USBCameraImpl::SendAndWait(
|
|
|
|
|
std::unique_ptr<Message> msg) const {
|
|
|
|
|
int fd = m_command_fd.load();
|
|
|
|
|
if (fd < 0) {
|
|
|
|
|
// not possible to signal, exit early
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use the message address as a unique identifier
|
|
|
|
|
Message* ptr = msg.get();
|
|
|
|
|
|
|
|
|
|
// Add the message to the command queue
|
2016-09-29 00:04:16 -07:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
2016-10-13 00:16:24 -07:00
|
|
|
m_commands.emplace_back(std::move(msg));
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
// Signal the camera thread
|
|
|
|
|
if (eventfd_write(fd, 1) < 0) return nullptr;
|
|
|
|
|
|
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
|
while (m_active) {
|
|
|
|
|
// Did we get a response to *our* request?
|
|
|
|
|
auto it = std::find_if(
|
|
|
|
|
m_responses.begin(), m_responses.end(),
|
|
|
|
|
[=](const std::unique_ptr<Message>& m) { return m.get() == ptr; });
|
|
|
|
|
if (it != m_responses.end()) {
|
|
|
|
|
// Yes, remove it from the vector and we're done.
|
|
|
|
|
auto rv = std::move(*it);
|
|
|
|
|
m_responses.erase(it);
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
// No, keep waiting for a response
|
|
|
|
|
m_responseCv.wait(lock);
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void USBCameraImpl::Send(std::unique_ptr<Message> msg) const {
|
|
|
|
|
int fd = m_command_fd.load();
|
|
|
|
|
if (fd < 0) {
|
|
|
|
|
// not possible to signal, exit early
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the message to the command queue
|
2016-09-29 00:04:16 -07:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
2016-10-13 00:16:24 -07:00
|
|
|
m_commands.emplace_back(std::move(msg));
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
|
|
|
|
|
// Signal the camera thread
|
|
|
|
|
eventfd_write(fd, 1);
|
2016-09-29 00:04:16 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
bool USBCameraImpl::CacheProperties(CS_Status* status) const {
|
|
|
|
|
// Wake up camera thread; this will try to reconnect
|
|
|
|
|
auto msg = CreateMessage(Message::kNone);
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) {
|
2016-09-29 00:04:16 -07:00
|
|
|
*status = CS_SOURCE_IS_DISCONNECTED;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2016-10-13 00:16:24 -07:00
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
if (!m_properties_cached) {
|
|
|
|
|
*status = CS_SOURCE_IS_DISCONNECTED;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2016-09-29 00:04:16 -07:00
|
|
|
|
2016-10-13 00:16:24 -07:00
|
|
|
bool USBCameraImpl::IsConnected() const { return m_fd >= 0; }
|
|
|
|
|
|
|
|
|
|
void USBCameraImpl::SetProperty(int property, int value, CS_Status* status) {
|
|
|
|
|
auto msg = CreateMessage(Message::kCmdSetProperty);
|
|
|
|
|
msg->data[0] = property;
|
|
|
|
|
msg->data[1] = value;
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) return;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kError) *status = msg->data[0];
|
2016-10-13 00:16:24 -07:00
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void USBCameraImpl::SetStringProperty(int property, llvm::StringRef value,
|
|
|
|
|
CS_Status* status) {
|
|
|
|
|
auto msg = CreateMessage(Message::kCmdSetPropertyStr);
|
|
|
|
|
msg->data[0] = property;
|
|
|
|
|
msg->dataStr = value;
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) return;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kError) *status = msg->data[0];
|
2016-10-13 00:16:24 -07:00
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool USBCameraImpl::SetVideoMode(const VideoMode& mode, CS_Status* status) {
|
|
|
|
|
auto msg = CreateMessage(Message::kCmdSetMode);
|
|
|
|
|
msg->data[0] = mode.pixelFormat;
|
|
|
|
|
msg->data[1] = mode.width;
|
|
|
|
|
msg->data[2] = mode.height;
|
|
|
|
|
msg->data[3] = mode.fps;
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) return false;
|
|
|
|
|
bool rv = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kError) {
|
2016-10-13 00:16:24 -07:00
|
|
|
*status = msg->data[0];
|
|
|
|
|
rv = false;
|
|
|
|
|
}
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool USBCameraImpl::SetPixelFormat(VideoMode::PixelFormat pixelFormat,
|
|
|
|
|
CS_Status* status) {
|
|
|
|
|
auto msg = CreateMessage(Message::kCmdSetPixelFormat);
|
|
|
|
|
msg->data[0] = pixelFormat;
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) return false;
|
|
|
|
|
bool rv = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kError) {
|
2016-10-13 00:16:24 -07:00
|
|
|
*status = msg->data[0];
|
|
|
|
|
rv = false;
|
|
|
|
|
}
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool USBCameraImpl::SetResolution(int width, int height, CS_Status* status) {
|
|
|
|
|
auto msg = CreateMessage(Message::kCmdSetResolution);
|
|
|
|
|
msg->data[0] = width;
|
|
|
|
|
msg->data[1] = height;
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) return false;
|
|
|
|
|
bool rv = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kError) {
|
2016-10-13 00:16:24 -07:00
|
|
|
*status = msg->data[0];
|
|
|
|
|
rv = false;
|
|
|
|
|
}
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool USBCameraImpl::SetFPS(int fps, CS_Status* status) {
|
|
|
|
|
auto msg = CreateMessage(Message::kCmdSetFPS);
|
|
|
|
|
msg->data[0] = fps;
|
|
|
|
|
msg = std::move(SendAndWait(std::move(msg)));
|
|
|
|
|
if (!msg) return false;
|
|
|
|
|
bool rv = true;
|
2016-11-15 23:54:50 -08:00
|
|
|
if (msg->kind == Message::kError) {
|
2016-10-13 00:16:24 -07:00
|
|
|
*status = msg->data[0];
|
|
|
|
|
rv = false;
|
|
|
|
|
}
|
|
|
|
|
DestroyMessage(std::move(msg));
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void USBCameraImpl::NumSinksChanged() {
|
|
|
|
|
Send(CreateMessage(Message::kNumSinksChanged));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void USBCameraImpl::NumSinksEnabledChanged() {
|
|
|
|
|
Send(CreateMessage(Message::kNumSinksEnabledChanged));
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace cs {
|
|
|
|
|
|
2016-10-15 23:07:28 -07:00
|
|
|
CS_Source CreateUSBCameraDev(llvm::StringRef name, int dev, CS_Status* status) {
|
2016-09-16 17:57:17 -07:00
|
|
|
llvm::SmallString<32> path;
|
|
|
|
|
llvm::raw_svector_ostream oss{path};
|
|
|
|
|
oss << "/dev/video" << dev;
|
2016-10-15 23:07:28 -07:00
|
|
|
return CreateUSBCameraPath(name, oss.str(), status);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-15 23:07:28 -07:00
|
|
|
CS_Source CreateUSBCameraPath(llvm::StringRef name, llvm::StringRef path,
|
2016-09-16 17:57:17 -07:00
|
|
|
CS_Status* status) {
|
|
|
|
|
auto source = std::make_shared<USBCameraImpl>(name, path);
|
2016-11-18 08:49:20 -08:00
|
|
|
auto handle = Sources::GetInstance().Allocate(CS_SOURCE_USB, source);
|
|
|
|
|
Notifier::GetInstance().NotifySource(name, handle, CS_SOURCE_CREATED);
|
2016-11-18 09:20:52 -08:00
|
|
|
// Start thread after the source created event to ensure other events
|
|
|
|
|
// come after it.
|
|
|
|
|
source->Start();
|
2016-11-18 08:49:20 -08:00
|
|
|
return handle;
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<USBCameraInfo> EnumerateUSBCameras(CS_Status* status) {
|
|
|
|
|
std::vector<USBCameraInfo> retval;
|
|
|
|
|
|
|
|
|
|
if (DIR* dp = opendir("/dev")) {
|
|
|
|
|
while (struct dirent* ep = readdir(dp)) {
|
|
|
|
|
llvm::StringRef fname{ep->d_name};
|
|
|
|
|
if (!fname.startswith("video")) continue;
|
|
|
|
|
|
|
|
|
|
USBCameraInfo info;
|
|
|
|
|
info.dev = -1;
|
|
|
|
|
fname.substr(5).getAsInteger(10, info.dev);
|
|
|
|
|
llvm::SmallString<32> path{"/dev/"};
|
|
|
|
|
path += fname;
|
|
|
|
|
info.path = path.str();
|
|
|
|
|
|
|
|
|
|
info.name = GetDescriptionImpl(path.c_str());
|
|
|
|
|
if (info.name.empty()) continue;
|
|
|
|
|
|
|
|
|
|
retval.emplace_back(std::move(info));
|
|
|
|
|
}
|
|
|
|
|
closedir(dp);
|
|
|
|
|
} else {
|
|
|
|
|
//*status = ;
|
|
|
|
|
ERROR("Could not open /dev");
|
|
|
|
|
return retval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sort by device number
|
|
|
|
|
std::sort(retval.begin(), retval.end(),
|
|
|
|
|
[](const USBCameraInfo& a, const USBCameraInfo& b) {
|
|
|
|
|
return a.dev < b.dev;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return retval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace cs
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
|
2016-10-15 23:07:28 -07:00
|
|
|
CS_Source CS_CreateUSBCameraDev(const char* name, int dev, CS_Status* status) {
|
|
|
|
|
return cs::CreateUSBCameraDev(name, dev, status);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-15 23:07:28 -07:00
|
|
|
CS_Source CS_CreateUSBCameraPath(const char* name, const char* path,
|
2016-09-16 17:57:17 -07:00
|
|
|
CS_Status* status) {
|
2016-10-15 23:07:28 -07:00
|
|
|
return cs::CreateUSBCameraPath(name, path, status);
|
2016-09-16 17:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CS_USBCameraInfo* CS_EnumerateUSBCameras(int* count, CS_Status* status) {
|
|
|
|
|
auto cameras = cs::EnumerateUSBCameras(status);
|
|
|
|
|
CS_USBCameraInfo* out = static_cast<CS_USBCameraInfo*>(
|
|
|
|
|
std::malloc(cameras.size() * sizeof(CS_USBCameraInfo)));
|
|
|
|
|
*count = cameras.size();
|
|
|
|
|
for (std::size_t i = 0; i < cameras.size(); ++i) {
|
|
|
|
|
out[i].dev = cameras[i].dev;
|
|
|
|
|
out[i].path = ConvertToC(cameras[i].path);
|
|
|
|
|
out[i].name = ConvertToC(cameras[i].name);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CS_FreeEnumeratedUSBCameras(CS_USBCameraInfo* cameras, int count) {
|
|
|
|
|
if (!cameras) return;
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
std::free(cameras[i].path);
|
|
|
|
|
std::free(cameras[i].name);
|
|
|
|
|
}
|
|
|
|
|
std::free(cameras);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // extern "C"
|
|
|
|
|
|
|
|
|
|
#endif // __linux__
|