| 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 |  | 
| 4 | #include "qgstreamerimagecapture_p.h" | 
| 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 |  | 
| 24 | QT_BEGIN_NAMESPACE | 
| 25 |  | 
| 26 | namespace { | 
| 27 | Q_LOGGING_CATEGORY(qLcImageCaptureGst, "qt.multimedia.imageCapture" ) | 
| 28 |  | 
| 29 | struct ThreadPoolSingleton | 
| 30 | { | 
| 31 |     QObject m_context; | 
| 32 |     QMutex m_poolMutex; | 
| 33 |     QThreadPool *m_instance{}; | 
| 34 |     bool m_appUnderDestruction = false; | 
| 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>(d: expiryTimeout).count()); | 
| 49 |  | 
| 50 |         QObject::connect(qApp, signal: &QCoreApplication::aboutToQuit, context: &m_context, slot: [&] { | 
| 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, signal: &QCoreApplication::destroyed, context: &m_context, slot: [&] { | 
| 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 |  | 
| 77 | ThreadPoolSingleton s_threadPoolSingleton; | 
| 78 |  | 
| 79 | }; // namespace | 
| 80 |  | 
| 81 | template <typename Functor> | 
| 82 | void QGstreamerImageCapture::invokeDeferred(Functor &&fn) | 
| 83 | { | 
| 84 |     QMetaObject::invokeMethod(this, std::forward<decltype(fn)>(fn), Qt::QueuedConnection); | 
| 85 | } | 
| 86 |  | 
| 87 | template <typename Functor> | 
| 88 | void QGstreamerImageCapture::runInThreadPool(Functor fn) | 
| 89 | { | 
| 90 |     int futureId = m_futureIDAllocator.fetch_add(i: 1, m: std::memory_order_relaxed); | 
| 91 |  | 
| 92 |     QFuture<void> future = QtConcurrent::run([this, futureId, fn = std::move(fn)]() mutable { | 
| 93 |         auto cleanup = qScopeGuard([&] { | 
| 94 |             QMutexLocker guard(&m_pendingFuturesMutex); | 
| 95 |             m_pendingFutures.erase(x: futureId); | 
| 96 |         }); | 
| 97 |         fn(); | 
| 98 |     }); | 
| 99 |  | 
| 100 |     if (!future.isValid()) // during qApplication shutdown the threadpool becomes unusable | 
| 101 |         return; | 
| 102 |  | 
| 103 |     QMutexLocker guard(&m_pendingFuturesMutex); | 
| 104 |     m_pendingFutures.emplace(args&: futureId, args: std::move(future)); | 
| 105 | } | 
| 106 |  | 
| 107 | QMaybe<QPlatformImageCapture *> QGstreamerImageCapture::create(QImageCapture *parent) | 
| 108 | { | 
| 109 |     static const auto error = qGstErrorMessageIfElementsNotAvailable( | 
| 110 |             arg: "queue" , args: "capsfilter" , args: "videoconvert" , args: "jpegenc" , args: "jifmux" , args: "fakesink" ); | 
| 111 |     if (error) | 
| 112 |         return *error; | 
| 113 |  | 
| 114 |     return new QGstreamerImageCapture(parent); | 
| 115 | } | 
| 116 |  | 
| 117 | QGstreamerImageCapture::QGstreamerImageCapture(QImageCapture *parent) | 
| 118 |     : QPlatformImageCapture(parent), | 
| 119 |       QGstreamerBufferProbe(ProbeBuffers), | 
| 120 |       bin{ | 
| 121 |           QGstBin::create(name: "imageCaptureBin" ), | 
| 122 |       }, | 
| 123 |       queue{ | 
| 124 |           QGstElement::createFromFactory(factory: "queue" , name: "imageCaptureQueue" ), | 
| 125 |       }, | 
| 126 |       filter{ | 
| 127 |           QGstElement::createFromFactory(factory: "capsfilter" , name: "filter" ), | 
| 128 |       }, | 
| 129 |       videoConvert{ | 
| 130 |           QGstElement::createFromFactory(factory: "videoconvert" , name: "imageCaptureConvert" ), | 
| 131 |       }, | 
| 132 |       encoder{ | 
| 133 |           QGstElement::createFromFactory(factory: "jpegenc" , name: "jpegEncoder" ), | 
| 134 |       }, | 
| 135 |       muxer{ | 
| 136 |           QGstElement::createFromFactory(factory: "jifmux" , name: "jpegMuxer" ), | 
| 137 |       }, | 
| 138 |       sink{ | 
| 139 |           QGstElement::createFromFactory(factory: "fakesink" , name: "imageCaptureSink" ), | 
| 140 |       } | 
| 141 | { | 
| 142 |     // configures the queue to be fast, lightweight and non blocking | 
| 143 |     queue.set(property: "leaky" , i: 2 /*downstream*/); | 
| 144 |     queue.set(property: "silent" , b: true); | 
| 145 |     queue.set(property: "max-size-buffers" , i: int(1)); | 
| 146 |     queue.set(property: "max-size-bytes" , i: int(0)); | 
| 147 |     queue.set(property: "max-size-time" , i: uint64_t(0)); | 
| 148 |  | 
| 149 |     bin.add(ts: queue, ts: filter, ts: videoConvert, ts: encoder, ts: muxer, ts: sink); | 
| 150 |     qLinkGstElements(ts: queue, ts: filter, ts: videoConvert, ts: encoder, ts: muxer, ts: sink); | 
| 151 |     bin.addGhostPad(child: queue, name: "sink" ); | 
| 152 |  | 
| 153 |     addProbeToPad(pad: queue.staticPad(name: "src" ).pad(), downstream: false); | 
| 154 |  | 
| 155 |     sink.set(property: "async" , b: false); | 
| 156 | } | 
| 157 |  | 
| 158 | QGstreamerImageCapture::~QGstreamerImageCapture() | 
| 159 | { | 
| 160 |     bin.setStateSync(state: GST_STATE_NULL); | 
| 161 |  | 
| 162 |     // wait for pending futures | 
| 163 |     auto pendingFutures = [&] { | 
| 164 |         QMutexLocker guard(&m_pendingFuturesMutex); | 
| 165 |         return std::move(m_pendingFutures); | 
| 166 |     }(); | 
| 167 |  | 
| 168 |     for (auto &element : pendingFutures) | 
| 169 |         element.second.waitForFinished(); | 
| 170 | } | 
| 171 |  | 
| 172 | bool QGstreamerImageCapture::isReadyForCapture() const | 
| 173 | { | 
| 174 |     QMutexLocker guard(&m_mutex); | 
| 175 |     return m_session && !m_captureNextBuffer && cameraActive; | 
| 176 | } | 
| 177 |  | 
| 178 | int QGstreamerImageCapture::capture(const QString &fileName) | 
| 179 | { | 
| 180 |     using namespace Qt::Literals; | 
| 181 |     QString path = QMediaStorageLocation::generateFileName( | 
| 182 |             requestedName: fileName, type: QStandardPaths::PicturesLocation, extension: u"jpg"_s ); | 
| 183 |     return doCapture(fileName: std::move(path)); | 
| 184 | } | 
| 185 |  | 
| 186 | int QGstreamerImageCapture::captureToBuffer() | 
| 187 | { | 
| 188 |     return doCapture(fileName: QString()); | 
| 189 | } | 
| 190 |  | 
| 191 | int QGstreamerImageCapture::doCapture(QString fileName) | 
| 192 | { | 
| 193 |     qCDebug(qLcImageCaptureGst) << "do capture" ; | 
| 194 |  | 
| 195 |     { | 
| 196 |         QMutexLocker guard(&m_mutex); | 
| 197 |         if (!m_session) { | 
| 198 |             invokeDeferred(fn: [this] { | 
| 199 |                 emit error(id: -1, error: QImageCapture::ResourceError, | 
| 200 |                            errorString: QPlatformImageCapture::msgImageCaptureNotSet()); | 
| 201 |             }); | 
| 202 |  | 
| 203 |             qCDebug(qLcImageCaptureGst) << "error 1" ; | 
| 204 |             return -1; | 
| 205 |         } | 
| 206 |         if (!m_session->camera()) { | 
| 207 |             invokeDeferred(fn: [this] { | 
| 208 |                 emit error(id: -1, error: QImageCapture::ResourceError, errorString: tr(s: "No camera available." )); | 
| 209 |             }); | 
| 210 |  | 
| 211 |             qCDebug(qLcImageCaptureGst) << "error 2" ; | 
| 212 |             return -1; | 
| 213 |         } | 
| 214 |         if (m_captureNextBuffer) { | 
| 215 |             invokeDeferred(fn: [this] { | 
| 216 |                 emit error(id: -1, error: QImageCapture::NotReadyError, | 
| 217 |                            errorString: QPlatformImageCapture::msgCameraNotReady()); | 
| 218 |             }); | 
| 219 |  | 
| 220 |             qCDebug(qLcImageCaptureGst) << "error 3" ; | 
| 221 |             return -1; | 
| 222 |         } | 
| 223 |         m_lastId++; | 
| 224 |  | 
| 225 |         pendingImages.enqueue(t: { .id: m_lastId, .filename: std::move(fileName) }); | 
| 226 |         // let one image pass the pipeline | 
| 227 |         m_captureNextBuffer = true; | 
| 228 |     } | 
| 229 |  | 
| 230 |     emit readyForCaptureChanged(ready: false); | 
| 231 |     return m_lastId; | 
| 232 | } | 
| 233 |  | 
| 234 | void QGstreamerImageCapture::saveBufferToFile(QGstBufferHandle buffer, QString filename, int taskId) | 
| 235 | { | 
| 236 |     Q_ASSERT(!filename.isEmpty()); | 
| 237 |  | 
| 238 |     runInThreadPool( | 
| 239 |             fn: [this, taskId, filename = std::move(filename), buffer = std::move(buffer)]() mutable { | 
| 240 |         QMutexLocker guard(&m_mutex); | 
| 241 |         qCDebug(qLcImageCaptureGst) << "saving image as"  << filename; | 
| 242 |  | 
| 243 |         QFile f(filename); | 
| 244 |         if (!f.open(flags: QFile::WriteOnly)) { | 
| 245 |             qCDebug(qLcImageCaptureGst) << "   could not open image file for writing" ; | 
| 246 |             return; | 
| 247 |         } | 
| 248 |  | 
| 249 |         GstMapInfo info; | 
| 250 |         if (gst_buffer_map(buffer: buffer.get(), info: &info, flags: GST_MAP_READ)) { | 
| 251 |             f.write(data: reinterpret_cast<const char *>(info.data), len: info.size); | 
| 252 |             gst_buffer_unmap(buffer: buffer.get(), info: &info); | 
| 253 |         } | 
| 254 |         f.close(); | 
| 255 |  | 
| 256 |         QMetaObject::invokeMethod(object: this, function: [this, taskId, filename = std::move(filename)]() mutable { | 
| 257 |             emit imageSaved(requestId: taskId, fileName: filename); | 
| 258 |         }); | 
| 259 |     }); | 
| 260 | } | 
| 261 | void QGstreamerImageCapture::convertBufferToImage(const QMutexLocker<QRecursiveMutex> &locker, | 
| 262 |                                                   QGstBufferHandle buffer, QGstCaps caps, | 
| 263 |                                                   QMediaMetaData metadata, int taskId) | 
| 264 | { | 
| 265 |     using namespace Qt::Literals; | 
| 266 |     Q_ASSERT(locker.mutex() == &m_mutex); | 
| 267 |     Q_ASSERT(locker.isLocked()); | 
| 268 |  | 
| 269 |     // QTBUG-131107: QVideoFrame::toImage() can only be called from the application thread | 
| 270 |     constexpr bool isOpenGLPlatform = QT_CONFIG(opengl); | 
| 271 |  | 
| 272 |     // QTBUG-130970: QVideoFrame::toImage() on worker thread causes wayland to crash on the | 
| 273 |     // application thread | 
| 274 |     static const bool isWaylandQPA = QGuiApplication::platformName() == u"wayland"_s ; | 
| 275 |  | 
| 276 |     if (isOpenGLPlatform || isWaylandQPA) { | 
| 277 |         if (!m_session) { | 
| 278 |             qDebug() << "QGstreamerImageCapture::convertBufferToImage: no session" ; | 
| 279 |             return; | 
| 280 |         } | 
| 281 |         auto memoryFormat = caps.memoryFormat(); | 
| 282 |  | 
| 283 |         GstVideoInfo previewInfo; | 
| 284 |         QVideoFrameFormat fmt; | 
| 285 |         auto optionalFormatAndVideoInfo = caps.formatAndVideoInfo(); | 
| 286 |         if (optionalFormatAndVideoInfo) | 
| 287 |             std::tie(args&: fmt, args&: previewInfo) = std::move(*optionalFormatAndVideoInfo); | 
| 288 |  | 
| 289 |         auto *sink = m_session->gstreamerVideoSink(); | 
| 290 |         auto gstBuffer = std::make_unique<QGstVideoBuffer>(args: std::move(buffer), args&: previewInfo, args&: sink, | 
| 291 |                                                            args&: fmt, args&: memoryFormat); | 
| 292 |         QVideoFrame frame = QVideoFramePrivate::createFrame(buffer: std::move(gstBuffer), format: fmt); | 
| 293 |  | 
| 294 |         metadata.insert(k: QMediaMetaData::Resolution, value: frame.size()); | 
| 295 |  | 
| 296 |         invokeDeferred( | 
| 297 |                 fn: [this, frame = std::move(frame), taskId, metadata = std::move(metadata)]() mutable { | 
| 298 |             QImage img = frame.toImage(); | 
| 299 |             if (img.isNull()) { | 
| 300 |                 qDebug() << "received a null image" ; | 
| 301 |                 return; | 
| 302 |             } | 
| 303 |  | 
| 304 |             emit imageExposed(requestId: taskId); | 
| 305 |             qCDebug(qLcImageCaptureGst) << "Image available!" ; | 
| 306 |             emit imageAvailable(requestId: taskId, buffer: frame); | 
| 307 |             emit imageCaptured(requestId: taskId, preview: img); | 
| 308 |             emit imageMetadataAvailable(id: taskId, metadata); | 
| 309 |         }); | 
| 310 |     } else { | 
| 311 |         runInThreadPool(fn: [this, taskId, buffer = std::move(buffer), caps = std::move(caps), | 
| 312 |                          metadata = std::move(metadata)]() mutable { | 
| 313 |             QMutexLocker guard(&m_mutex); | 
| 314 |             if (!m_session) { | 
| 315 |                 qDebug() << "QGstreamerImageCapture::probeBuffer: no session" ; | 
| 316 |                 return; | 
| 317 |             } | 
| 318 |  | 
| 319 |             auto memoryFormat = caps.memoryFormat(); | 
| 320 |  | 
| 321 |             GstVideoInfo previewInfo; | 
| 322 |             QVideoFrameFormat fmt; | 
| 323 |             auto optionalFormatAndVideoInfo = caps.formatAndVideoInfo(); | 
| 324 |             if (optionalFormatAndVideoInfo) | 
| 325 |                 std::tie(args&: fmt, args&: previewInfo) = std::move(*optionalFormatAndVideoInfo); | 
| 326 |  | 
| 327 |             auto *sink = m_session->gstreamerVideoSink(); | 
| 328 |             auto gstBuffer = std::make_unique<QGstVideoBuffer>(args: std::move(buffer), args&: previewInfo, args&: sink, | 
| 329 |                                                                args&: fmt, args&: memoryFormat); | 
| 330 |  | 
| 331 |             QVideoFrame frame = QVideoFramePrivate::createFrame(buffer: std::move(gstBuffer), format: fmt); | 
| 332 |             QImage img = frame.toImage(); | 
| 333 |             if (img.isNull()) { | 
| 334 |                 qDebug() << "received a null image" ; | 
| 335 |                 return; | 
| 336 |             } | 
| 337 |  | 
| 338 |             QMediaMetaData imageMetaData = metaData(); | 
| 339 |             imageMetaData.insert(k: QMediaMetaData::Resolution, value: frame.size()); | 
| 340 |  | 
| 341 |             invokeDeferred(fn: [this, taskId, metadata = std::move(metadata), frame = std::move(frame), | 
| 342 |                             img = std::move(img)]() mutable { | 
| 343 |                 emit imageExposed(requestId: taskId); | 
| 344 |                 qCDebug(qLcImageCaptureGst) << "Image available!" ; | 
| 345 |                 emit imageAvailable(requestId: taskId, buffer: frame); | 
| 346 |                 emit imageCaptured(requestId: taskId, preview: img); | 
| 347 |                 emit imageMetadataAvailable(id: taskId, metadata); | 
| 348 |             }); | 
| 349 |         }); | 
| 350 |     } | 
| 351 | } | 
| 352 |  | 
| 353 | void QGstreamerImageCapture::setResolution(const QSize &resolution) | 
| 354 | { | 
| 355 |     QGstCaps padCaps = bin.staticPad(name: "sink" ).currentCaps(); | 
| 356 |     if (!padCaps) { | 
| 357 |         qDebug() << "Camera not ready" ; | 
| 358 |         return; | 
| 359 |     } | 
| 360 |     QGstCaps caps = padCaps.copy(); | 
| 361 |     if (!caps) | 
| 362 |         return; | 
| 363 |  | 
| 364 |     gst_caps_set_simple(caps: caps.caps(), field: "width" , G_TYPE_INT, resolution.width(), "height" , G_TYPE_INT, | 
| 365 |                         resolution.height(), nullptr); | 
| 366 |     filter.set(property: "caps" , c: caps); | 
| 367 | } | 
| 368 |  | 
| 369 | bool QGstreamerImageCapture::probeBuffer(GstBuffer *buffer) | 
| 370 | { | 
| 371 |     if (!m_captureNextBuffer.load()) | 
| 372 |         return false; | 
| 373 |  | 
| 374 |     QMutexLocker guard(&m_mutex); | 
| 375 |     qCDebug(qLcImageCaptureGst) << "probe buffer" ; | 
| 376 |  | 
| 377 |     QGstBufferHandle bufferHandle{ | 
| 378 |         buffer, | 
| 379 |         QGstBufferHandle::NeedsRef, | 
| 380 |     }; | 
| 381 |  | 
| 382 |     m_captureNextBuffer = false; | 
| 383 |  | 
| 384 |     bool ready = isReadyForCapture(); | 
| 385 |     invokeDeferred(fn: [this, ready] { | 
| 386 |         emit readyForCaptureChanged(ready); | 
| 387 |     }); | 
| 388 |  | 
| 389 |     // save file | 
| 390 |     PendingImage imageData = pendingImages.dequeue(); | 
| 391 |     QString saveFileName = imageData.filename; | 
| 392 |     if (!saveFileName.isEmpty()) | 
| 393 |         saveBufferToFile(buffer: bufferHandle, filename: std::move(saveFileName), taskId: imageData.id); | 
| 394 |  | 
| 395 |     // convert to image and emit | 
| 396 |     QGstCaps caps = bin.staticPad(name: "sink" ).currentCaps(); | 
| 397 |     QMediaMetaData imageMetaData = metaData(); | 
| 398 |     convertBufferToImage(locker: guard, buffer: bufferHandle, caps: std::move(caps), metadata: std::move(imageMetaData), | 
| 399 |                          taskId: imageData.id); | 
| 400 |  | 
| 401 |     return true; | 
| 402 | } | 
| 403 |  | 
| 404 | void QGstreamerImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session) | 
| 405 | { | 
| 406 |     QMutexLocker guard(&m_mutex); | 
| 407 |     QGstreamerMediaCaptureSession *captureSession = static_cast<QGstreamerMediaCaptureSession *>(session); | 
| 408 |     if (m_session == captureSession) | 
| 409 |         return; | 
| 410 |  | 
| 411 |     bool readyForCapture = isReadyForCapture(); | 
| 412 |     if (m_session) { | 
| 413 |         disconnect(sender: m_session, signal: nullptr, receiver: this, member: nullptr); | 
| 414 |         m_lastId = 0; | 
| 415 |         pendingImages.clear(); | 
| 416 |         m_captureNextBuffer = false; | 
| 417 |         cameraActive = false; | 
| 418 |     } | 
| 419 |  | 
| 420 |     m_session = captureSession; | 
| 421 |     if (!m_session) { | 
| 422 |         if (readyForCapture) | 
| 423 |             emit readyForCaptureChanged(ready: false); | 
| 424 |         return; | 
| 425 |     } | 
| 426 |  | 
| 427 |     connect(sender: m_session, signal: &QPlatformMediaCaptureSession::cameraChanged, context: this, | 
| 428 |             slot: &QGstreamerImageCapture::onCameraChanged); | 
| 429 |     onCameraChanged(); | 
| 430 | } | 
| 431 |  | 
| 432 | void QGstreamerImageCapture::setMetaData(const QMediaMetaData &m) | 
| 433 | { | 
| 434 |     { | 
| 435 |         QMutexLocker guard(&m_mutex); | 
| 436 |         QPlatformImageCapture::setMetaData(m); | 
| 437 |     } | 
| 438 |  | 
| 439 |     // ensure taginject injects this metaData | 
| 440 |     applyMetaDataToTagSetter(metadata: m, muxer); | 
| 441 | } | 
| 442 |  | 
| 443 | void QGstreamerImageCapture::cameraActiveChanged(bool active) | 
| 444 | { | 
| 445 |     qCDebug(qLcImageCaptureGst) << "cameraActiveChanged"  << cameraActive << active; | 
| 446 |     if (cameraActive == active) | 
| 447 |         return; | 
| 448 |     cameraActive = active; | 
| 449 |     qCDebug(qLcImageCaptureGst) << "isReady"  << isReadyForCapture(); | 
| 450 |     emit readyForCaptureChanged(ready: isReadyForCapture()); | 
| 451 | } | 
| 452 |  | 
| 453 | void QGstreamerImageCapture::onCameraChanged() | 
| 454 | { | 
| 455 |     QMutexLocker guard(&m_mutex); | 
| 456 |     if (m_session->camera()) { | 
| 457 |         cameraActiveChanged(active: m_session->camera()->isActive()); | 
| 458 |         connect(sender: m_session->camera(), signal: &QPlatformCamera::activeChanged, context: this, | 
| 459 |                 slot: &QGstreamerImageCapture::cameraActiveChanged); | 
| 460 |     } else { | 
| 461 |         cameraActiveChanged(active: false); | 
| 462 |     } | 
| 463 | } | 
| 464 |  | 
| 465 | QImageEncoderSettings QGstreamerImageCapture::imageSettings() const | 
| 466 | { | 
| 467 |     return m_settings; | 
| 468 | } | 
| 469 |  | 
| 470 | void QGstreamerImageCapture::setImageSettings(const QImageEncoderSettings &settings) | 
| 471 | { | 
| 472 |     if (m_settings != settings) { | 
| 473 |         QSize resolution = settings.resolution(); | 
| 474 |         if (m_settings.resolution() != resolution && !resolution.isEmpty()) | 
| 475 |             setResolution(resolution); | 
| 476 |  | 
| 477 |         m_settings = settings; | 
| 478 |     } | 
| 479 | } | 
| 480 |  | 
| 481 | QT_END_NAMESPACE | 
| 482 |  | 
| 483 | #include "moc_qgstreamerimagecapture_p.cpp" | 
| 484 |  |