Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgstreamerimagecapture.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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
5
6#include <QtMultimedia/qvideoframeformat.h>
7#include <QtMultimedia/private/qmediastoragelocation_p.h>
8#include <QtMultimedia/private/qplatformcamera_p.h>
9#include <QtMultimedia/private/qplatformimagecapture_p.h>
10#include <QtMultimedia/private/qvideoframe_p.h>
11#include <QtGui/qguiapplication.h>
12#include <QtCore/qdebug.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qstandardpaths.h>
15#include <QtCore/qcoreapplication.h>
16#include <QtCore/qloggingcategory.h>
17
18#include <common/qgstreamermetadata_p.h>
19#include <common/qgstvideobuffer_p.h>
20#include <common/qgstutils_p.h>
21
22#include <utility>
23
24QT_BEGIN_NAMESPACE
25
26namespace {
27
28Q_STATIC_LOGGING_CATEGORY(qLcImageCaptureGst, "qt.multimedia.imageCapture")
29
31{
36
37 QThreadPool *get(const QMutexLocker<QMutex> &)
38 {
39 if (m_instance)
40 return m_instance;
41 if (m_appUnderDestruction || !qApp)
42 return nullptr;
43
44 using namespace std::chrono;
45
46 m_instance = new QThreadPool;
47 m_instance->setMaxThreadCount(1); // 1 thread;
48 static constexpr auto expiryTimeout = minutes(5);
49 m_instance->setExpiryTimeout(round<milliseconds>(expiryTimeout).count());
50
51 QObject::connect(qApp, &QCoreApplication::aboutToQuit, &m_context, [&] {
52 // we need to make sure that thread-local QRhi is destroyed before the application to
53 // prevent QTBUG-124189
54 QMutexLocker guard(&m_poolMutex);
55 delete m_instance;
56 m_instance = {};
57 m_appUnderDestruction = true;
58 });
59
60 QObject::connect(qApp, &QCoreApplication::destroyed, &m_context, [&] {
61 m_appUnderDestruction = false;
62 });
63 return m_instance;
64 }
65
66 template <typename Functor>
67 QFuture<void> run(Functor &&f)
68 {
69 QMutexLocker guard(&m_poolMutex);
70 QThreadPool *pool = get(guard);
71 if (!pool)
72 return QFuture<void>{};
73
74 return QtConcurrent::run(pool, std::forward<Functor>(f));
75 }
76};
77
79
80}; // namespace
81
82template <typename Functor>
83void QGstreamerImageCapture::invokeDeferred(Functor &&fn)
84{
85 QMetaObject::invokeMethod(this, std::forward<decltype(fn)>(fn), Qt::QueuedConnection);
86}
87
88template <typename Functor>
89void QGstreamerImageCapture::runInThreadPool(Functor fn)
90{
91 int futureId = m_futureIDAllocator.fetch_add(1, std::memory_order_relaxed);
92
93 QFuture<void> future = QtConcurrent::run([this, futureId, fn = std::move(fn)]() mutable {
94 auto cleanup = qScopeGuard([&] {
95 QMutexLocker guard(&m_pendingFuturesMutex);
96 m_pendingFutures.erase(futureId);
97 });
98 fn();
99 });
100
101 if (!future.isValid()) // during qApplication shutdown the threadpool becomes unusable
102 return;
103
104 QMutexLocker guard(&m_pendingFuturesMutex);
105 m_pendingFutures.emplace(futureId, std::move(future));
106}
107
108q23::expected<QPlatformImageCapture *, QString> QGstreamerImageCapture::create(QImageCapture *parent)
109{
110 static const auto error = qGstErrorMessageIfElementsNotAvailable(
111 "queue", "capsfilter", "videoconvert", "jpegenc", "jifmux", "fakesink");
112 if (error)
113 return q23::unexpected{ *error };
114
115 return new QGstreamerImageCapture(parent);
116}
117
118QGstreamerImageCapture::QGstreamerImageCapture(QImageCapture *parent)
119 : QPlatformImageCapture(parent),
120 QGstreamerBufferProbe(ProbeBuffers),
121 bin{
122 QGstBin::create("imageCaptureBin"),
123 },
124 queue{
125 QGstElement::createFromFactory("queue", "imageCaptureQueue"),
126 },
127 filter{
128 QGstElement::createFromFactory("capsfilter", "filter"),
129 },
130 videoConvert{
131 QGstElement::createFromFactory("videoconvert", "imageCaptureConvert"),
132 },
133 encoder{
134 QGstElement::createFromFactory("jpegenc", "jpegEncoder"),
135 },
136 muxer{
137 QGstElement::createFromFactory("jifmux", "jpegMuxer"),
138 },
139 sink{
140 QGstElement::createFromFactory("fakesink", "imageCaptureSink"),
141 }
142{
143 // configures the queue to be fast, lightweight and non blocking
144 queue.set("leaky", 2 /*downstream*/);
145 queue.set("silent", true);
146 queue.set("max-size-buffers", int(1));
147 queue.set("max-size-bytes", int(0));
148 queue.set("max-size-time", uint64_t(0));
149
150 bin.add(queue, filter, videoConvert, encoder, muxer, sink);
151 qLinkGstElements(queue, filter, videoConvert, encoder, muxer, sink);
152 bin.addGhostPad(queue, "sink");
153
154 addProbeToPad(queue.staticPad("src").pad(), false);
155
156 sink.set("async", false);
157}
158
159QGstreamerImageCapture::~QGstreamerImageCapture()
160{
161 bin.setStateSync(GST_STATE_NULL);
162
163 // wait for pending futures
164 auto pendingFutures = [&] {
165 QMutexLocker guard(&m_pendingFuturesMutex);
166 return std::move(m_pendingFutures);
167 }();
168
169 for (auto &element : pendingFutures)
170 element.second.waitForFinished();
171}
172
173bool QGstreamerImageCapture::isReadyForCapture() const
174{
175 QMutexLocker guard(&m_mutex);
176 return m_session && !m_captureNextBuffer && cameraActive;
177}
178
179int QGstreamerImageCapture::capture(const QString &fileName)
180{
181 using namespace Qt::Literals;
182 QString path = QMediaStorageLocation::generateFileName(
183 fileName, QStandardPaths::PicturesLocation, u"jpg"_s);
184 return doCapture(std::move(path));
185}
186
187int QGstreamerImageCapture::captureToBuffer()
188{
189 return doCapture(QString());
190}
191
192int QGstreamerImageCapture::doCapture(QString fileName)
193{
194 qCDebug(qLcImageCaptureGst) << "do capture";
195
196 {
197 QMutexLocker guard(&m_mutex);
198 if (!m_session) {
199 invokeDeferred([this] {
200 Q_EMIT error(-1, QImageCapture::ResourceError,
201 QPlatformImageCapture::msgImageCaptureNotSet());
202 });
203
204 qCDebug(qLcImageCaptureGst) << "error 1";
205 return -1;
206 }
207 if (!m_session->camera()) {
208 invokeDeferred([this] {
209 Q_EMIT error(-1, QImageCapture::ResourceError, tr("No camera available."));
210 });
211
212 qCDebug(qLcImageCaptureGst) << "error 2";
213 return -1;
214 }
215 if (m_captureNextBuffer) {
216 invokeDeferred([this] {
217 Q_EMIT error(-1, QImageCapture::NotReadyError,
218 QPlatformImageCapture::msgCameraNotReady());
219 });
220
221 qCDebug(qLcImageCaptureGst) << "error 3";
222 return -1;
223 }
224 m_lastId++;
225
226 pendingImages.enqueue({ m_lastId, std::move(fileName) });
227 // let one image pass the pipeline
228 m_captureNextBuffer = true;
229 }
230
231 Q_EMIT readyForCaptureChanged(false);
232 return m_lastId;
233}
234
235void QGstreamerImageCapture::saveBufferToFile(QGstBufferHandle buffer, QString filename, int taskId)
236{
237 Q_ASSERT(!filename.isEmpty());
238
239 runInThreadPool(
240 [this, taskId, filename = std::move(filename), buffer = std::move(buffer)]() mutable {
241 QMutexLocker guard(&m_mutex);
242 qCDebug(qLcImageCaptureGst) << "saving image as" << filename;
243
244 QFile f(filename);
245 if (!f.open(QFile::WriteOnly)) {
246 qCDebug(qLcImageCaptureGst) << " could not open image file for writing";
247 return;
248 }
249
250 GstMapInfo info;
251 if (gst_buffer_map(buffer.get(), &info, GST_MAP_READ)) {
252 f.write(reinterpret_cast<const char *>(info.data), info.size);
253 gst_buffer_unmap(buffer.get(), &info);
254 }
255 f.close();
256
257 QMetaObject::invokeMethod(this, [this, taskId, filename = std::move(filename)]() mutable {
258 Q_EMIT imageSaved(taskId, filename);
259 });
260 });
261}
262void QGstreamerImageCapture::convertBufferToImage(const QMutexLocker<QRecursiveMutex> &locker,
263 QGstBufferHandle buffer, QGstCaps caps,
264 QMediaMetaData metadata, int taskId)
265{
266 using namespace Qt::Literals;
267 Q_ASSERT(locker.mutex() == &m_mutex);
268 Q_ASSERT(locker.isLocked());
269
270 // QTBUG-131107: QVideoFrame::toImage() can only be called from the application thread
271 constexpr bool isOpenGLPlatform = QT_CONFIG(opengl);
272
273 // QTBUG-130970: QVideoFrame::toImage() on worker thread causes wayland to crash on the
274 // application thread
275 static const bool isWaylandQPA = QGuiApplication::platformName() == u"wayland"_s;
276
277 if (isOpenGLPlatform || isWaylandQPA) {
278 if (!m_session) {
279 qDebug() << "QGstreamerImageCapture::convertBufferToImage: no session";
280 return;
281 }
282
283 QGstVideoInfo previewInfo;
284 auto optionalVideoInfo = caps.videoInfo();
285 if (optionalVideoInfo)
286 previewInfo = *optionalVideoInfo;
287 QVideoFrame frame = qCreateFrameFromGstBuffer(buffer, previewInfo);
288
289 metadata.insert(QMediaMetaData::Resolution, frame.size());
290
291 invokeDeferred(
292 [this, frame = std::move(frame), taskId, metadata = std::move(metadata)]() mutable {
293 QImage img = frame.toImage();
294 if (img.isNull()) {
295 qDebug() << "received a null image";
296 return;
297 }
298
299 Q_EMIT imageExposed(taskId);
300 qCDebug(qLcImageCaptureGst) << "Image available!";
301 Q_EMIT imageAvailable(taskId, frame);
302 Q_EMIT imageCaptured(taskId, img);
303 Q_EMIT imageMetadataAvailable(taskId, metadata);
304 });
305 } else {
306 runInThreadPool([this, taskId, buffer = std::move(buffer), caps = std::move(caps),
307 metadata = std::move(metadata)]() mutable {
308 QMutexLocker guard(&m_mutex);
309 if (!m_session) {
310 qDebug() << "QGstreamerImageCapture::probeBuffer: no session";
311 return;
312 }
313
314 QGstVideoInfo previewInfo;
315 QVideoFrameFormat fmt;
316 auto optionalVideoInfo = caps.videoInfo();
317 if (optionalVideoInfo) {
318 previewInfo = *optionalVideoInfo;
319 fmt = qVideoFrameFormatFromGstVideoInfo(previewInfo);
320 }
321
322 QVideoFrame frame = qCreateFrameFromGstBuffer(std::move(buffer), previewInfo, fmt);
323 QImage img = frame.toImage();
324 if (img.isNull()) {
325 qDebug() << "received a null image";
326 return;
327 }
328
329 QMediaMetaData imageMetaData = metaData();
330 imageMetaData.insert(QMediaMetaData::Resolution, frame.size());
331
332 invokeDeferred([this, taskId, metadata = std::move(metadata), frame = std::move(frame),
333 img = std::move(img)]() mutable {
334 Q_EMIT imageExposed(taskId);
335 qCDebug(qLcImageCaptureGst) << "Image available!";
336 Q_EMIT imageAvailable(taskId, frame);
337 Q_EMIT imageCaptured(taskId, img);
338 Q_EMIT imageMetadataAvailable(taskId, metadata);
339 });
340 });
341 }
342}
343
344void QGstreamerImageCapture::setResolution(const QSize &resolution)
345{
346 QGstCaps padCaps = bin.staticPad("sink").currentCaps();
347 if (!padCaps) {
348 qDebug() << "Camera not ready";
349 return;
350 }
351 QGstCaps caps = padCaps.copy();
352 if (!caps)
353 return;
354
355 gst_caps_set_simple(caps.caps(), "width", G_TYPE_INT, resolution.width(), "height", G_TYPE_INT,
356 resolution.height(), nullptr);
357 filter.set("caps", caps);
358}
359
360bool QGstreamerImageCapture::probeBuffer(GstBuffer *buffer)
361{
362 if (!m_captureNextBuffer.load())
363 return false;
364
365 QMutexLocker guard(&m_mutex);
366 qCDebug(qLcImageCaptureGst) << "probe buffer";
367
368 QGstBufferHandle bufferHandle{
369 buffer,
370 QGstBufferHandle::NeedsRef,
371 };
372
373 m_captureNextBuffer = false;
374
375 bool ready = isReadyForCapture();
376 invokeDeferred([this, ready] {
377 Q_EMIT readyForCaptureChanged(ready);
378 });
379
380 // save file
381 PendingImage imageData = pendingImages.dequeue();
382 QString saveFileName = imageData.filename;
383 if (!saveFileName.isEmpty())
384 saveBufferToFile(bufferHandle, std::move(saveFileName), imageData.id);
385
386 // convert to image and emit
387 QGstCaps caps = bin.staticPad("sink").currentCaps();
388 QMediaMetaData imageMetaData = metaData();
389 convertBufferToImage(guard, bufferHandle, std::move(caps), std::move(imageMetaData),
390 imageData.id);
391
392 return true;
393}
394
395void QGstreamerImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session)
396{
397 QMutexLocker guard(&m_mutex);
398 QGstreamerMediaCaptureSession *captureSession = static_cast<QGstreamerMediaCaptureSession *>(session);
399 if (m_session == captureSession)
400 return;
401
402 bool readyForCapture = isReadyForCapture();
403 if (m_session) {
404 disconnect(m_session, nullptr, this, nullptr);
405 m_lastId = 0;
406 pendingImages.clear();
407 m_captureNextBuffer = false;
408 cameraActive = false;
409 }
410
411 m_session = captureSession;
412 if (!m_session) {
413 if (readyForCapture)
414 Q_EMIT readyForCaptureChanged(false);
415 return;
416 }
417
418 connect(m_session, &QPlatformMediaCaptureSession::cameraChanged, this,
419 &QGstreamerImageCapture::onCameraChanged);
420 onCameraChanged();
421}
422
423void QGstreamerImageCapture::setMetaData(const QMediaMetaData &m)
424{
425 {
426 QMutexLocker guard(&m_mutex);
427 QPlatformImageCapture::setMetaData(m);
428 }
429
430 // ensure taginject injects this metaData
431 applyMetaDataToTagSetter(m, muxer);
432}
433
434void QGstreamerImageCapture::cameraActiveChanged(bool active)
435{
436 qCDebug(qLcImageCaptureGst) << "cameraActiveChanged" << cameraActive << active;
437 if (cameraActive == active)
438 return;
439 cameraActive = active;
440 qCDebug(qLcImageCaptureGst) << "isReady" << isReadyForCapture();
441 Q_EMIT readyForCaptureChanged(isReadyForCapture());
442}
443
444void QGstreamerImageCapture::onCameraChanged()
445{
446 QMutexLocker guard(&m_mutex);
447 if (m_session->camera()) {
448 cameraActiveChanged(m_session->camera()->isActive());
449 connect(m_session->camera(), &QPlatformCamera::activeChanged, this,
450 &QGstreamerImageCapture::cameraActiveChanged);
451 } else {
452 cameraActiveChanged(false);
453 }
454}
455
456QImageEncoderSettings QGstreamerImageCapture::imageSettings() const
457{
458 return m_settings;
459}
460
461void QGstreamerImageCapture::setImageSettings(const QImageEncoderSettings &settings)
462{
463 if (m_settings != settings) {
464 QSize resolution = settings.resolution();
465 if (m_settings.resolution() != resolution && !resolution.isEmpty())
466 setResolution(resolution);
467
468 m_settings = settings;
469 }
470}
471
472QT_END_NAMESPACE
473
474#include "moc_qgstreamerimagecapture_p.cpp"
QGstCaps copy() const
Definition qgst.cpp:602
\inmodule QtMultimedia
ThreadPoolSingleton s_threadPoolSingleton
typedefQT_BEGIN_NAMESPACE struct _GstBuffer GstBuffer
QThreadPool * get(const QMutexLocker< QMutex > &)