| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qgstreamervideodevices_p.h" |
| 5 | |
| 6 | #include <QtMultimedia/qmediadevices.h> |
| 7 | #include <QtMultimedia/private/qcameradevice_p.h> |
| 8 | #include <QtCore/qloggingcategory.h> |
| 9 | #include <QtCore/qset.h> |
| 10 | #include <QtCore/private/quniquehandle_types_p.h> |
| 11 | |
| 12 | #include <common/qgst_p.h> |
| 13 | #include <common/qgst_debug_p.h> |
| 14 | #include <common/qgstutils_p.h> |
| 15 | #include <common/qglist_helper_p.h> |
| 16 | |
| 17 | #if QT_CONFIG(linux_v4l) |
| 18 | # include <linux/videodev2.h> |
| 19 | # include <errno.h> |
| 20 | #endif |
| 21 | |
| 22 | QT_BEGIN_NAMESPACE |
| 23 | |
| 24 | Q_STATIC_LOGGING_CATEGORY(ltVideoDevices, "qt.multimedia.gstreamer.videodevices" ); |
| 25 | |
| 26 | QGstreamerVideoDevices::QGstreamerVideoDevices(QPlatformMediaIntegration *integration) |
| 27 | : QPlatformVideoDevices(integration), |
| 28 | m_deviceMonitor{ |
| 29 | gst_device_monitor_new(), |
| 30 | QGstDeviceMonitorHandle::HasRef, |
| 31 | }, |
| 32 | m_busObserver{ |
| 33 | QGstBusHandle{ |
| 34 | gst_device_monitor_get_bus(monitor: m_deviceMonitor.get()), |
| 35 | QGstBusHandle::HasRef, |
| 36 | }, |
| 37 | } |
| 38 | { |
| 39 | gst_device_monitor_add_filter(monitor: m_deviceMonitor.get(), classes: "Video/Source" , caps: nullptr); |
| 40 | |
| 41 | m_busObserver.installMessageFilter(this); |
| 42 | gst_device_monitor_start(monitor: m_deviceMonitor.get()); |
| 43 | |
| 44 | GList *devices = gst_device_monitor_get_devices(monitor: m_deviceMonitor.get()); |
| 45 | |
| 46 | for (GstDevice *device : QGstUtils::GListRangeAdaptor<GstDevice *>(devices)) { |
| 47 | addDevice(QGstDeviceHandle{ |
| 48 | device, |
| 49 | QGstDeviceHandle::HasRef, |
| 50 | }); |
| 51 | } |
| 52 | |
| 53 | g_list_free(list: devices); |
| 54 | } |
| 55 | |
| 56 | QGstreamerVideoDevices::~QGstreamerVideoDevices() |
| 57 | { |
| 58 | gst_device_monitor_stop(monitor: m_deviceMonitor.get()); |
| 59 | } |
| 60 | |
| 61 | QList<QCameraDevice> QGstreamerVideoDevices::findVideoInputs() const |
| 62 | { |
| 63 | QList<QCameraDevice> devices; |
| 64 | |
| 65 | for (const auto &device : m_videoSources) { |
| 66 | QCameraDevicePrivate *info = new QCameraDevicePrivate; |
| 67 | |
| 68 | QGString desc{ |
| 69 | gst_device_get_display_name(device: device.gstDevice.get()), |
| 70 | }; |
| 71 | info->description = desc.toQString(); |
| 72 | info->id = device.id; |
| 73 | |
| 74 | QUniqueGstStructureHandle properties{ |
| 75 | gst_device_get_properties(device: device.gstDevice.get()), |
| 76 | }; |
| 77 | if (properties) { |
| 78 | QGstStructureView view{ properties }; |
| 79 | auto def = view["is-default" ].toBool(); |
| 80 | info->isDefault = def && *def; |
| 81 | } |
| 82 | |
| 83 | if (info->isDefault) |
| 84 | devices.prepend(t: info->create()); |
| 85 | else |
| 86 | devices.append(t: info->create()); |
| 87 | |
| 88 | auto caps = QGstCaps(gst_device_get_caps(device: device.gstDevice.get()), QGstCaps::HasRef); |
| 89 | if (caps) { |
| 90 | QList<QCameraFormat> formats; |
| 91 | QSet<QSize> photoResolutions; |
| 92 | |
| 93 | int size = caps.size(); |
| 94 | for (int i = 0; i < size; ++i) { |
| 95 | auto cap = caps.at(index: i); |
| 96 | auto pixelFormat = cap.pixelFormat(); |
| 97 | auto frameRate = cap.frameRateRange(); |
| 98 | |
| 99 | if (pixelFormat == QVideoFrameFormat::PixelFormat::Format_Invalid) { |
| 100 | qCDebug(ltVideoDevices) << "pixel format not supported:" << cap; |
| 101 | continue; // skip pixel formats that we don't support |
| 102 | } |
| 103 | |
| 104 | auto addFormatForResolution = [&](QSize resolution) { |
| 105 | auto *f = new QCameraFormatPrivate{ |
| 106 | QSharedData(), .pixelFormat: pixelFormat, .resolution: resolution, .minFrameRate: frameRate.min, .maxFrameRate: frameRate.max, |
| 107 | }; |
| 108 | formats.append(t: f->create()); |
| 109 | photoResolutions.insert(value: resolution); |
| 110 | }; |
| 111 | |
| 112 | std::optional<QGRange<QSize>> resolutionRange = cap.resolutionRange(); |
| 113 | if (resolutionRange) { |
| 114 | addFormatForResolution(resolutionRange->min); |
| 115 | addFormatForResolution(resolutionRange->max); |
| 116 | } else { |
| 117 | QSize resolution = cap.resolution(); |
| 118 | if (resolution.isValid()) |
| 119 | addFormatForResolution(resolution); |
| 120 | } |
| 121 | } |
| 122 | info->videoFormats = formats; |
| 123 | // ### sort resolutions? |
| 124 | info->photoResolutions = photoResolutions.values(); |
| 125 | } |
| 126 | } |
| 127 | return devices; |
| 128 | } |
| 129 | |
| 130 | void QGstreamerVideoDevices::addDevice(QGstDeviceHandle device) |
| 131 | { |
| 132 | Q_ASSERT(gst_device_has_classes(device.get(), "Video/Source" )); |
| 133 | |
| 134 | #if QT_CONFIG(linux_v4l) |
| 135 | QUniqueGstStructureHandle structureHandle{ |
| 136 | gst_device_get_properties(device: device.get()), |
| 137 | }; |
| 138 | |
| 139 | const auto *p = QGstStructureView(structureHandle.get())["device.path" ].toString(); |
| 140 | if (p) { |
| 141 | QUniqueFileDescriptorHandle fd{ |
| 142 | qt_safe_open(pathname: p, O_RDONLY), |
| 143 | }; |
| 144 | |
| 145 | if (!fd) { |
| 146 | qCDebug(ltVideoDevices) << "Cannot open v4l2 device:" << p; |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | struct v4l2_capability cap; |
| 151 | if (::ioctl(fd: fd.get(), VIDIOC_QUERYCAP, &cap) < 0) { |
| 152 | qCWarning(ltVideoDevices) |
| 153 | << "ioctl failed: VIDIOC_QUERYCAP" << qt_error_string(errno) << p; |
| 154 | return; |
| 155 | } |
| 156 | |
| 157 | if (cap.device_caps & V4L2_CAP_META_CAPTURE) { |
| 158 | qCDebug(ltVideoDevices) << "V4L2_CAP_META_CAPTURE device detected" << p; |
| 159 | return; |
| 160 | } |
| 161 | |
| 162 | constexpr uint32_t videoCaptureCapabilities = |
| 163 | V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_CAPTURE_MPLANE; |
| 164 | |
| 165 | if (!(cap.capabilities & videoCaptureCapabilities)) { |
| 166 | qCDebug(ltVideoDevices) |
| 167 | << "not a V4L2_CAP_VIDEO_CAPTURE or V4L2_CAP_VIDEO_CAPTURE_MPLANE device" << p; |
| 168 | return; |
| 169 | } |
| 170 | if (!(cap.capabilities & V4L2_CAP_STREAMING)) { |
| 171 | qCDebug(ltVideoDevices) << "not a V4L2_CAP_STREAMING device" << p; |
| 172 | return; |
| 173 | } |
| 174 | |
| 175 | int index; |
| 176 | if (::ioctl(fd: fd.get(), VIDIOC_G_INPUT, &index) < 0) { |
| 177 | switch (errno) { |
| 178 | case ENOTTY: |
| 179 | qCDebug(ltVideoDevices) << "device does not have video inputs" << p; |
| 180 | return; |
| 181 | |
| 182 | default: |
| 183 | qCWarning(ltVideoDevices) |
| 184 | << "ioctl failed: VIDIOC_G_INPUT" << qt_error_string(errno) << p; |
| 185 | return; |
| 186 | } |
| 187 | } |
| 188 | } else { |
| 189 | qCDebug(ltVideoDevices) << "Video device not a v4l2 device:" << structureHandle; |
| 190 | } |
| 191 | #endif |
| 192 | |
| 193 | auto it = std::find_if(first: m_videoSources.begin(), last: m_videoSources.end(), |
| 194 | pred: [&](const QGstRecordDevice &a) { return a.gstDevice == device; }); |
| 195 | |
| 196 | if (it != m_videoSources.end()) |
| 197 | return; |
| 198 | |
| 199 | m_videoSources.push_back(x: QGstRecordDevice{ |
| 200 | .gstDevice: std::move(device), |
| 201 | .id: QByteArray::number(m_idGenerator), |
| 202 | }); |
| 203 | |
| 204 | m_idGenerator++; |
| 205 | |
| 206 | onVideoInputsChanged(); |
| 207 | } |
| 208 | |
| 209 | void QGstreamerVideoDevices::removeDevice(QGstDeviceHandle device) |
| 210 | { |
| 211 | auto it = std::find_if(first: m_videoSources.begin(), last: m_videoSources.end(), |
| 212 | pred: [&](const QGstRecordDevice &a) { return a.gstDevice == device; }); |
| 213 | |
| 214 | if (it != m_videoSources.end()) { |
| 215 | m_videoSources.erase(position: it); |
| 216 | onVideoInputsChanged(); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | bool QGstreamerVideoDevices::processBusMessage(const QGstreamerMessage &message) |
| 221 | { |
| 222 | QGstDeviceHandle device; |
| 223 | |
| 224 | switch (message.type()) { |
| 225 | case GST_MESSAGE_DEVICE_ADDED: |
| 226 | gst_message_parse_device_added(message: message.message(), device: &device); |
| 227 | addDevice(device: std::move(device)); |
| 228 | break; |
| 229 | case GST_MESSAGE_DEVICE_REMOVED: |
| 230 | gst_message_parse_device_removed(message: message.message(), device: &device); |
| 231 | removeDevice(device: std::move(device)); |
| 232 | break; |
| 233 | default: |
| 234 | break; |
| 235 | } |
| 236 | |
| 237 | return false; |
| 238 | } |
| 239 | |
| 240 | GstDevice *QGstreamerVideoDevices::videoDevice(const QByteArray &id) const |
| 241 | { |
| 242 | auto it = std::find_if(first: m_videoSources.begin(), last: m_videoSources.end(), |
| 243 | pred: [&](const QGstRecordDevice &a) { return a.id == id; }); |
| 244 | return it != m_videoSources.end() ? it->gstDevice.get() : nullptr; |
| 245 | } |
| 246 | |
| 247 | QT_END_NAMESPACE |
| 248 | |