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