Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qgstreamervideodevices_p.h"
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/private/qcameradevice_p.h>
#include <QtMultimedia/private/qmultimedia_ranges_p.h>
#include <QtMultimedia/private/qmultimediautils_p.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qset.h>
#include <QtCore/private/quniquehandle_types_p.h>
#include <common/qgst_p.h>
#include <common/qgst_debug_p.h>
#include <common/qgstutils_p.h>
#include <common/qglist_helper_p.h>
#if QT_CONFIG(linux_v4l)
# include <linux/videodev2.h>
# include <errno.h>
#endif
QT_BEGIN_NAMESPACE
namespace ranges = QtMultimediaPrivate::ranges;
Q_STATIC_LOGGING_CATEGORY(ltVideoDevices, "qt.multimedia.gstreamer.videodevices");
QGstreamerVideoDevices::QGstreamerVideoDevices(QPlatformMediaIntegration *integration)
: QPlatformVideoDevices(integration),
m_deviceMonitor{
gst_device_monitor_new(),
QGstDeviceMonitorHandle::HasRef,
},
m_busObserver{
QGstBusHandle{
gst_device_monitor_get_bus(m_deviceMonitor.get()),
QGstBusHandle::HasRef,
},
}
{
gst_device_monitor_add_filter(m_deviceMonitor.get(), "Video/Source", nullptr);
gst_device_monitor_set_show_all_devices(m_deviceMonitor.get(), true);
m_busObserver.installMessageFilter(this);
gst_device_monitor_start(m_deviceMonitor.get());
GList *devices = gst_device_monitor_get_devices(m_deviceMonitor.get());
for (GstDevice *device : QGstUtils::GListRangeAdaptor<GstDevice *>(devices)) {
addDevice(QGstDeviceHandle{
device,
QGstDeviceHandle::HasRef,
});
}
g_list_free(devices);
}
QGstreamerVideoDevices::~QGstreamerVideoDevices()
{
gst_device_monitor_stop(m_deviceMonitor.get());
}
QList<QCameraDevice> QGstreamerVideoDevices::findVideoInputs() const
{
QList<QCameraDevice> devices;
for (const auto &device : m_videoSources) {
QCameraDevicePrivate *info = new QCameraDevicePrivate;
QGString desc{
gst_device_get_display_name(device.gstDevice.get()),
};
info->description = desc.toQString();
info->id = device.id;
QUniqueGstStructureHandle properties{
gst_device_get_properties(device.gstDevice.get()),
};
if (properties) {
QGstStructureView view{ properties };
auto def = view["is-default"].toBool();
info->isDefault = def && *def;
}
if (info->isDefault)
devices.prepend(info->create());
else
devices.append(info->create());
auto caps = QGstCaps(gst_device_get_caps(device.gstDevice.get()), QGstCaps::HasRef);
if (caps) {
QList<QCameraFormat> formats;
QSet<QSize> photoResolutions;
int size = caps.size();
for (int i = 0; i < size; ++i) {
auto cap = caps.at(i);
QList<QVideoFrameFormat::PixelFormat> pixelFormats = cap.pixelFormats();
auto frameRate = cap.frameRateRange();
if (pixelFormats.isEmpty()) {
qCDebug(ltVideoDevices) << "pixel format(s) not supported:" << cap;
continue; // skip pixel formats that we don't support
}
auto addFormatForResolution = [&](QSize resolution) {
for (QVideoFrameFormat::PixelFormat pixelFormat : std::as_const(pixelFormats)){
auto *f = new QCameraFormatPrivate{
QSharedData(), pixelFormat, resolution, frameRate.min, frameRate.max,
};
formats.append(f->create());
}
photoResolutions.insert(resolution);
};
std::optional<QGRange<QSize>> resolutionRange = cap.resolutionRange();
if (resolutionRange) {
addFormatForResolution(resolutionRange->min);
addFormatForResolution(resolutionRange->max);
} else {
QSize resolution = cap.resolution();
if (resolution.isValid())
addFormatForResolution(resolution);
}
}
info->videoFormats = formats;
// ### sort resolutions?
info->photoResolutions = photoResolutions.values();
}
}
return devices;
}
void QGstreamerVideoDevices::addDevice(QGstDeviceHandle device)
{
Q_ASSERT(gst_device_has_classes(device.get(), "Video/Source"));
#if QT_CONFIG(linux_v4l)
QUniqueGstStructureHandle propertiesHandle{
gst_device_get_properties(device.get()),
};
if (!propertiesHandle.isValid()) {
qCDebug(ltVideoDevices) << "Skipping device without extra properties:" << device.get();
return;
}
auto properties = QGstStructureView(propertiesHandle.get());
// Pipewire devices causes infinite futex wait in gst_pipewire_src_change_state after calling
// QGstreamerMediaCaptureSession::setCameraActive() with true, so we skip adding them:
if (properties.name().contains("pipewire")) {
qCDebug(ltVideoDevices) << "Skipping pipewire device:" << device.get();
return;
}
// QTBUG-140092: NXP's CSI video device "imx-capture" may fail. Can be skipped via env var:
static const bool skipImxCapture = qEnvironmentVariableIsSet("QT_GSTREAMER_SKIP_IMXCAPTURE");
if (skipImxCapture) {
const char *name = properties["device.product.name"].toString();
if (name && std::strstr(name, "imx-capture")) {
qWarning() << Q_FUNC_INFO << "Skipping video device with product name" << name;
return;
}
}
const auto *p = properties["device.path"].toString();
if (p) {
QUniqueFileDescriptorHandle fd{
qt_safe_open(p, O_RDONLY),
};
if (!fd) {
qCDebug(ltVideoDevices) << "Cannot open v4l2 device:" << p;
return;
}
struct v4l2_capability cap;
if (QtMultimediaPrivate::qt_safe_ioctl(fd.get(), VIDIOC_QUERYCAP, &cap) < 0) {
qCWarning(ltVideoDevices)
<< "ioctl failed: VIDIOC_QUERYCAP" << qt_error_string(errno) << p;
return;
}
if (cap.device_caps & V4L2_CAP_META_CAPTURE) {
qCDebug(ltVideoDevices) << "V4L2_CAP_META_CAPTURE device detected" << p;
return;
}
constexpr uint32_t videoCaptureCapabilities =
V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_CAPTURE_MPLANE;
if (!(cap.capabilities & videoCaptureCapabilities)) {
qCDebug(ltVideoDevices)
<< "not a V4L2_CAP_VIDEO_CAPTURE or V4L2_CAP_VIDEO_CAPTURE_MPLANE device" << p;
return;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
qCDebug(ltVideoDevices) << "not a V4L2_CAP_STREAMING device" << p;
return;
}
int index;
if (QtMultimediaPrivate::qt_safe_ioctl(fd.get(), VIDIOC_G_INPUT, &index) < 0) {
switch (errno) {
case ENOTTY:
qCDebug(ltVideoDevices) << "Device does not support VIDIOC_G_INPUT, but it could"
"still work" << p;
break;
default:
qCWarning(ltVideoDevices)
<< "ioctl failed: VIDIOC_G_INPUT" << qt_error_string(errno) << p;
return;
}
}
} else {
qCDebug(ltVideoDevices) << "Video device not a v4l2 device:" << propertiesHandle;
}
#endif
auto it = ranges::find_if(m_videoSources, [&](const QGstRecordDevice &a) {
return a.gstDevice == device;
});
if (it != m_videoSources.end())
return;
m_videoSources.push_back(QGstRecordDevice{
std::move(device),
QByteArray::number(m_idGenerator),
});
m_idGenerator++;
onVideoInputsChanged();
}
void QGstreamerVideoDevices::removeDevice(QGstDeviceHandle device)
{
auto it = ranges::find_if(m_videoSources, [&](const QGstRecordDevice &a) {
return a.gstDevice == device;
});
if (it != m_videoSources.end()) {
m_videoSources.erase(it);
onVideoInputsChanged();
}
}
bool QGstreamerVideoDevices::processBusMessage(const QGstreamerMessage &message)
{
QGstDeviceHandle device;
switch (message.type()) {
case GST_MESSAGE_DEVICE_ADDED:
gst_message_parse_device_added(message.message(), &device);
addDevice(std::move(device));
break;
case GST_MESSAGE_DEVICE_REMOVED:
gst_message_parse_device_removed(message.message(), &device);
removeDevice(std::move(device));
break;
default:
break;
}
return false;
}
GstDevice *QGstreamerVideoDevices::videoDevice(const QByteArray &id) const
{
auto it = ranges::find_if(m_videoSources, [&](const QGstRecordDevice &a) {
return a.id == id;
});
return it != m_videoSources.end() ? it->gstDevice.get() : nullptr;
}
QT_END_NAMESPACE