Revision control
++ // 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