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 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 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 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 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 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 emit imageExposed(taskId);
300 qCDebug(qLcImageCaptureGst) << "Image available!";
301 emit imageAvailable(taskId, frame);
302 emit imageCaptured(taskId, img);
303 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 auto gstBuffer = std::make_unique<QGstVideoBuffer>(std::move(buffer), previewInfo, fmt);
323
324 QVideoFrame frame = QVideoFramePrivate::createFrame(std::move(gstBuffer), fmt);
325 QImage img = frame.toImage();
326 if (img.isNull()) {
327 qDebug() << "received a null image";
328 return;
329 }
330
331 QMediaMetaData imageMetaData = metaData();
332 imageMetaData.insert(QMediaMetaData::Resolution, frame.size());
333
334 invokeDeferred([this, taskId, metadata = std::move(metadata), frame = std::move(frame),
335 img = std::move(img)]() mutable {
336 emit imageExposed(taskId);
337 qCDebug(qLcImageCaptureGst) << "Image available!";
338 emit imageAvailable(taskId, frame);
339 emit imageCaptured(taskId, img);
340 emit imageMetadataAvailable(taskId, metadata);
341 });
342 });
343 }
344}
345
346void QGstreamerImageCapture::setResolution(const QSize &resolution)
347{
348 QGstCaps padCaps = bin.staticPad("sink").currentCaps();
349 if (!padCaps) {
350 qDebug() << "Camera not ready";
351 return;
352 }
353 QGstCaps caps = padCaps.copy();
354 if (!caps)
355 return;
356
357 gst_caps_set_simple(caps.caps(), "width", G_TYPE_INT, resolution.width(), "height", G_TYPE_INT,
358 resolution.height(), nullptr);
359 filter.set("caps", caps);
360}
361
362bool QGstreamerImageCapture::probeBuffer(GstBuffer *buffer)
363{
364 if (!m_captureNextBuffer.load())
365 return false;
366
367 QMutexLocker guard(&m_mutex);
368 qCDebug(qLcImageCaptureGst) << "probe buffer";
369
370 QGstBufferHandle bufferHandle{
371 buffer,
372 QGstBufferHandle::NeedsRef,
373 };
374
375 m_captureNextBuffer = false;
376
377 bool ready = isReadyForCapture();
378 invokeDeferred([this, ready] {
379 emit readyForCaptureChanged(ready);
380 });
381
382 // save file
383 PendingImage imageData = pendingImages.dequeue();
384 QString saveFileName = imageData.filename;
385 if (!saveFileName.isEmpty())
386 saveBufferToFile(bufferHandle, std::move(saveFileName), imageData.id);
387
388 // convert to image and emit
389 QGstCaps caps = bin.staticPad("sink").currentCaps();
390 QMediaMetaData imageMetaData = metaData();
391 convertBufferToImage(guard, bufferHandle, std::move(caps), std::move(imageMetaData),
392 imageData.id);
393
394 return true;
395}
396
397void QGstreamerImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session)
398{
399 QMutexLocker guard(&m_mutex);
400 QGstreamerMediaCaptureSession *captureSession = static_cast<QGstreamerMediaCaptureSession *>(session);
401 if (m_session == captureSession)
402 return;
403
404 bool readyForCapture = isReadyForCapture();
405 if (m_session) {
406 disconnect(m_session, nullptr, this, nullptr);
407 m_lastId = 0;
408 pendingImages.clear();
409 m_captureNextBuffer = false;
410 cameraActive = false;
411 }
412
413 m_session = captureSession;
414 if (!m_session) {
415 if (readyForCapture)
416 emit readyForCaptureChanged(false);
417 return;
418 }
419
420 connect(m_session, &QPlatformMediaCaptureSession::cameraChanged, this,
421 &QGstreamerImageCapture::onCameraChanged);
422 onCameraChanged();
423}
424
425void QGstreamerImageCapture::setMetaData(const QMediaMetaData &m)
426{
427 {
428 QMutexLocker guard(&m_mutex);
429 QPlatformImageCapture::setMetaData(m);
430 }
431
432 // ensure taginject injects this metaData
433 applyMetaDataToTagSetter(m, muxer);
434}
435
436void QGstreamerImageCapture::cameraActiveChanged(bool active)
437{
438 qCDebug(qLcImageCaptureGst) << "cameraActiveChanged" << cameraActive << active;
439 if (cameraActive == active)
440 return;
441 cameraActive = active;
442 qCDebug(qLcImageCaptureGst) << "isReady" << isReadyForCapture();
443 emit readyForCaptureChanged(isReadyForCapture());
444}
445
446void QGstreamerImageCapture::onCameraChanged()
447{
448 QMutexLocker guard(&m_mutex);
449 if (m_session->camera()) {
450 cameraActiveChanged(m_session->camera()->isActive());
451 connect(m_session->camera(), &QPlatformCamera::activeChanged, this,
452 &QGstreamerImageCapture::cameraActiveChanged);
453 } else {
454 cameraActiveChanged(false);
455 }
456}
457
458QImageEncoderSettings QGstreamerImageCapture::imageSettings() const
459{
460 return m_settings;
461}
462
463void QGstreamerImageCapture::setImageSettings(const QImageEncoderSettings &settings)
464{
465 if (m_settings != settings) {
466 QSize resolution = settings.resolution();
467 if (m_settings.resolution() != resolution && !resolution.isEmpty())
468 setResolution(resolution);
469
470 m_settings = settings;
471 }
472}
473
474QT_END_NAMESPACE
475
476#include "moc_qgstreamerimagecapture_p.cpp"
QGstCaps copy() const
Definition qgst.cpp:594
\inmodule QtMultimedia
ThreadPoolSingleton s_threadPoolSingleton
QThreadPool * get(const QMutexLocker< QMutex > &)