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