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