Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qffmpegimagecapture_p.h"
#include <private/qplatformmediaformatinfo_p.h>
#include <private/qplatformcamera_p.h>
#include <private/qplatformimagecapture_p.h>
#include <qvideoframeformat.h>
#include <private/qmediastoragelocation_p.h>
#include <qimagewriter.h>
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <qstandardpaths.h>
#include <qloggingcategory.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
// Probably, might be increased. To be investigated and tested on Android implementation
static constexpr int MaxPendingImagesCount = 1;
Q_STATIC_LOGGING_CATEGORY(qLcImageCapture, "qt.multimedia.imageCapture")
QFFmpegImageCapture::QFFmpegImageCapture(QImageCapture *parent)
: QPlatformImageCapture(parent)
{
qRegisterMetaType<QVideoFrame>();
}
QFFmpegImageCapture::~QFFmpegImageCapture() = default;
bool QFFmpegImageCapture::isReadyForCapture() const
{
return m_isReadyForCapture;
}
static const char *extensionForFormat(QImageCapture::FileFormat format)
{
const char *fmt = "jpg";
switch (format) {
case QImageCapture::UnspecifiedFormat:
case QImageCapture::JPEG:
fmt = "jpg";
break;
case QImageCapture::PNG:
fmt = "png";
break;
case QImageCapture::WebP:
fmt = "webp";
break;
case QImageCapture::Tiff:
fmt = "tiff";
break;
}
return fmt;
}
int QFFmpegImageCapture::capture(const QString &fileName)
{
QString path = QMediaStorageLocation::generateFileName(fileName, QStandardPaths::PicturesLocation, QLatin1String(extensionForFormat(m_settings.format())));
return doCapture(path);
}
int QFFmpegImageCapture::captureToBuffer()
{
return doCapture(QString());
}
int QFFmpegImageCapture::doCapture(const QString &fileName)
{
using namespace Qt::Literals::StringLiterals;
if (!m_session) {
//emit error in the next event loop,
//so application can associate it with returned request id.
QMetaObject::invokeMethod(
this,
[this]() {
Q_EMIT error(
-1,
QImageCapture::ResourceError,
QPlatformImageCapture::msgImageCaptureNotSet());
});
return -1;
}
if (!m_videoSource) {
//emit error in the next event loop,
//so application can associate it with returned request id.
QMetaObject::invokeMethod(
this,
[this]() {
Q_EMIT error(
-1,
QImageCapture::ResourceError,
u"No camera available."_s);
});
return -1;
}
if (m_pendingImages.size() >= MaxPendingImagesCount) {
//emit error in the next event loop,
//so application can associate it with returned request id.
QMetaObject::invokeMethod(
this,
[this]() {
Q_EMIT error(
-1,
QImageCapture::NotReadyError,
QPlatformImageCapture::msgCameraNotReady());
});
return -1;
}
m_lastId++;
m_pendingImages.enqueue({ m_lastId, fileName, QMediaMetaData{} });
updateReadyForCapture();
return m_lastId;
}
void QFFmpegImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session)
{
auto *captureSession = static_cast<QFFmpegMediaCaptureSession *>(session);
if (m_session == captureSession)
return;
if (m_session) {
m_session->disconnect(this);
m_lastId = 0;
m_pendingImages.clear();
}
m_session = captureSession;
if (m_session) {
connect(m_session, &QFFmpegMediaCaptureSession::primaryActiveVideoSourceChanged, this,
&QFFmpegImageCapture::onVideoSourceChanged);
connect(m_session, &QFFmpegMediaCaptureSession::cameraChanged, this,
&QFFmpegImageCapture::applySettingsToVideoSource);
}
onVideoSourceChanged();
if (m_session)
applySettingsToVideoSource();
}
void QFFmpegImageCapture::cancelPendingImage(QImageCapture::Error error, const QString &errorMsg)
{
if (m_pendingImages.empty()) {
qCDebug(qLcImageCapture) <<
"QImageCapture backend received an event to cancel a pending capture, "
"but no captures are pending. Most likely an internal Qt bug.";
return;
}
PendingImage cancelledImage = m_pendingImages.dequeue();
Q_EMIT QPlatformImageCapture::error(cancelledImage.id, error, errorMsg);
updateReadyForCapture();
}
void QFFmpegImageCapture::updateReadyForCapture()
{
const bool ready = m_session && m_pendingImages.size() < MaxPendingImagesCount && m_videoSource
&& m_videoSource->isActive();
qCDebug(qLcImageCapture) << "updateReadyForCapture" << ready;
if (std::exchange(m_isReadyForCapture, ready) != ready)
Q_EMIT readyForCaptureChanged(ready);
}
void QFFmpegImageCapture::newVideoFrame(const QVideoFrame &frame)
{
if (m_pendingImages.empty())
return;
auto pending = m_pendingImages.dequeue();
qCDebug(qLcImageCapture) << "Taking image" << pending.id;
Q_EMIT imageExposed(pending.id);
// ### Add metadata from the AVFrame
Q_EMIT imageMetadataAvailable(pending.id, pending.metaData);
Q_EMIT imageAvailable(pending.id, frame);
QImage image = frame.toImage();
// Assume image is rotated if its orientation differs from the requested resolution
const QSize resolution = m_settings.resolution();
const bool isRotated = image.width() != image.height()
&& resolution.width() != resolution.height()
&& (image.width() > image.height()) != (resolution.width() > resolution.height());
const QSize targetSize = isRotated ? resolution.transposed() : resolution;
if (targetSize.isValid() && image.size() != targetSize) {
qCWarning(qLcImageCapture)
<< "Image needed to be scaled to match requested resolution (" << image.size()
<< " -> " << targetSize
<< ", consider using a resolution supported natively by the camera";
image = image.scaled(targetSize);
}
Q_EMIT imageCaptured(pending.id, image);
if (!pending.filename.isEmpty()) {
const char *fmt = nullptr;
switch (m_settings.format()) {
case QImageCapture::UnspecifiedFormat:
case QImageCapture::JPEG:
fmt = "jpeg";
break;
case QImageCapture::PNG:
fmt = "png";
break;
case QImageCapture::WebP:
fmt = "webp";
break;
case QImageCapture::Tiff:
fmt = "tiff";
break;
}
int quality = -1;
switch (m_settings.quality()) {
case QImageCapture::VeryLowQuality:
quality = 25;
break;
case QImageCapture::LowQuality:
quality = 50;
break;
case QImageCapture::NormalQuality:
break;
case QImageCapture::HighQuality:
quality = 75;
break;
case QImageCapture::VeryHighQuality:
quality = 99;
break;
}
QImageWriter writer(pending.filename, fmt);
writer.setQuality(quality);
if (writer.write(image)) {
Q_EMIT imageSaved(pending.id, pending.filename);
} else {
QImageCapture::Error err = QImageCapture::ResourceError;
if (writer.error() == QImageWriter::UnsupportedFormatError)
err = QImageCapture::FormatError;
Q_EMIT error(pending.id, err, writer.errorString());
}
}
updateReadyForCapture();
}
void QFFmpegImageCapture::setupVideoSourceConnections()
{
connect(m_videoSource, &QPlatformCamera::newVideoFrame, this,
&QFFmpegImageCapture::newVideoFrame);
}
QPlatformVideoSource *QFFmpegImageCapture::videoSource() const
{
return m_videoSource;
}
void QFFmpegImageCapture::onVideoSourceChanged()
{
if (m_videoSource)
m_videoSource->disconnect(this);
m_videoSource = m_session ? m_session->primaryActiveVideoSource() : nullptr;
// TODO: optimize, setup the connection only when the capture is ready
if (m_videoSource) {
setupVideoSourceConnections();
applySettingsToVideoSource();
}
updateReadyForCapture();
}
QImageEncoderSettings QFFmpegImageCapture::imageSettings() const
{
return m_settings;
}
void QFFmpegImageCapture::setImageSettings(const QImageEncoderSettings &settings)
{
auto s = settings;
const auto supportedFormats = QPlatformMediaIntegration::instance()->formatInfo()->imageFormats;
if (supportedFormats.isEmpty()) {
Q_EMIT error(-1, QImageCapture::FormatError, u"No image formats supported, can't capture."_s);
return;
}
if (s.format() == QImageCapture::UnspecifiedFormat) {
auto f = QImageCapture::JPEG;
if (!supportedFormats.contains(f))
f = supportedFormats.first();
s.setFormat(f);
} else if (!supportedFormats.contains(settings.format())) {
Q_EMIT error(-1, QImageCapture::FormatError, u"Image format not supported."_s);
return;
}
m_settings = settings;
applySettingsToVideoSource();
}
QT_END_NAMESPACE
#include "moc_qffmpegimagecapture_p.cpp"