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 "qffmpegimagecapture_p.h" |
5 | #include <private/qplatformmediaformatinfo_p.h> |
6 | #include <private/qplatformcamera_p.h> |
7 | #include <private/qplatformimagecapture_p.h> |
8 | #include <qvideoframeformat.h> |
9 | #include <private/qmediastoragelocation_p.h> |
10 | #include <qimagewriter.h> |
11 | |
12 | #include <QtCore/QDebug> |
13 | #include <QtCore/QDir> |
14 | #include <qstandardpaths.h> |
15 | |
16 | #include <qloggingcategory.h> |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | // Probably, might be increased. To be investigated and tested on Android implementation |
21 | static constexpr int MaxPendingImagesCount = 1; |
22 | |
23 | static Q_LOGGING_CATEGORY(qLcImageCapture, "qt.multimedia.imageCapture" ) |
24 | |
25 | QFFmpegImageCapture::QFFmpegImageCapture(QImageCapture *parent) |
26 | : QPlatformImageCapture(parent) |
27 | { |
28 | qRegisterMetaType<QVideoFrame>(); |
29 | } |
30 | |
31 | QFFmpegImageCapture::~QFFmpegImageCapture() |
32 | { |
33 | } |
34 | |
35 | bool QFFmpegImageCapture::isReadyForCapture() const |
36 | { |
37 | return m_isReadyForCapture; |
38 | } |
39 | |
40 | static const char *extensionForFormat(QImageCapture::FileFormat format) |
41 | { |
42 | const char *fmt = "jpg" ; |
43 | switch (format) { |
44 | case QImageCapture::UnspecifiedFormat: |
45 | case QImageCapture::JPEG: |
46 | fmt = "jpg" ; |
47 | break; |
48 | case QImageCapture::PNG: |
49 | fmt = "png" ; |
50 | break; |
51 | case QImageCapture::WebP: |
52 | fmt = "webp" ; |
53 | break; |
54 | case QImageCapture::Tiff: |
55 | fmt = "tiff" ; |
56 | break; |
57 | } |
58 | return fmt; |
59 | } |
60 | |
61 | int QFFmpegImageCapture::capture(const QString &fileName) |
62 | { |
63 | QString path = QMediaStorageLocation::generateFileName(requestedName: fileName, type: QStandardPaths::PicturesLocation, extension: QLatin1String(extensionForFormat(format: m_settings.format()))); |
64 | return doCapture(fileName: path); |
65 | } |
66 | |
67 | int QFFmpegImageCapture::captureToBuffer() |
68 | { |
69 | return doCapture(fileName: QString()); |
70 | } |
71 | |
72 | int QFFmpegImageCapture::doCapture(const QString &fileName) |
73 | { |
74 | qCDebug(qLcImageCapture) << "do capture" ; |
75 | if (!m_session) { |
76 | //emit error in the next event loop, |
77 | //so application can associate it with returned request id. |
78 | QMetaObject::invokeMethod(obj: this, member: "error" , c: Qt::QueuedConnection, |
79 | Q_ARG(int, -1), |
80 | Q_ARG(int, QImageCapture::ResourceError), |
81 | Q_ARG(QString, QPlatformImageCapture::msgImageCaptureNotSet())); |
82 | |
83 | qCDebug(qLcImageCapture) << "error 1" ; |
84 | return -1; |
85 | } |
86 | if (!m_videoSource) { |
87 | //emit error in the next event loop, |
88 | //so application can associate it with returned request id. |
89 | QMetaObject::invokeMethod(obj: this, member: "error" , c: Qt::QueuedConnection, |
90 | Q_ARG(int, -1), |
91 | Q_ARG(int, QImageCapture::ResourceError), |
92 | Q_ARG(QString,tr("No camera available." ))); |
93 | |
94 | qCDebug(qLcImageCapture) << "error 2" ; |
95 | return -1; |
96 | } |
97 | if (m_pendingImages.size() >= MaxPendingImagesCount) { |
98 | //emit error in the next event loop, |
99 | //so application can associate it with returned request id. |
100 | QMetaObject::invokeMethod(obj: this, member: "error" , c: Qt::QueuedConnection, |
101 | Q_ARG(int, -1), |
102 | Q_ARG(int, QImageCapture::NotReadyError), |
103 | Q_ARG(QString, QPlatformImageCapture::msgCameraNotReady())); |
104 | |
105 | qCDebug(qLcImageCapture) << "error 3" ; |
106 | return -1; |
107 | } |
108 | |
109 | m_lastId++; |
110 | |
111 | m_pendingImages.enqueue(t: { .id: m_lastId, .filename: fileName, .metaData: QMediaMetaData{} }); |
112 | updateReadyForCapture(); |
113 | |
114 | return m_lastId; |
115 | } |
116 | |
117 | void QFFmpegImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session) |
118 | { |
119 | auto *captureSession = static_cast<QFFmpegMediaCaptureSession *>(session); |
120 | if (m_session == captureSession) |
121 | return; |
122 | |
123 | if (m_session) { |
124 | m_session->disconnect(receiver: this); |
125 | m_lastId = 0; |
126 | m_pendingImages.clear(); |
127 | } |
128 | |
129 | m_session = captureSession; |
130 | |
131 | if (m_session) |
132 | connect(sender: m_session, signal: &QFFmpegMediaCaptureSession::primaryActiveVideoSourceChanged, context: this, |
133 | slot: &QFFmpegImageCapture::onVideoSourceChanged); |
134 | |
135 | onVideoSourceChanged(); |
136 | } |
137 | |
138 | void QFFmpegImageCapture::updateReadyForCapture() |
139 | { |
140 | const bool ready = m_session && m_pendingImages.size() < MaxPendingImagesCount && m_videoSource |
141 | && m_videoSource->isActive(); |
142 | |
143 | qCDebug(qLcImageCapture) << "updateReadyForCapture" << ready; |
144 | |
145 | if (std::exchange(obj&: m_isReadyForCapture, new_val: ready) != ready) |
146 | emit readyForCaptureChanged(ready); |
147 | } |
148 | |
149 | void QFFmpegImageCapture::newVideoFrame(const QVideoFrame &frame) |
150 | { |
151 | if (m_pendingImages.empty()) |
152 | return; |
153 | |
154 | auto pending = m_pendingImages.dequeue(); |
155 | |
156 | qCDebug(qLcImageCapture) << "Taking image" << pending.id; |
157 | |
158 | emit imageExposed(requestId: pending.id); |
159 | // ### Add metadata from the AVFrame |
160 | emit imageMetadataAvailable(id: pending.id, pending.metaData); |
161 | emit imageAvailable(requestId: pending.id, buffer: frame); |
162 | QImage image = frame.toImage(); |
163 | if (m_settings.resolution().isValid() && m_settings.resolution() != image.size()) |
164 | image = image.scaled(s: m_settings.resolution()); |
165 | |
166 | emit imageCaptured(requestId: pending.id, preview: image); |
167 | if (!pending.filename.isEmpty()) { |
168 | const char *fmt = nullptr; |
169 | switch (m_settings.format()) { |
170 | case QImageCapture::UnspecifiedFormat: |
171 | case QImageCapture::JPEG: |
172 | fmt = "jpeg" ; |
173 | break; |
174 | case QImageCapture::PNG: |
175 | fmt = "png" ; |
176 | break; |
177 | case QImageCapture::WebP: |
178 | fmt = "webp" ; |
179 | break; |
180 | case QImageCapture::Tiff: |
181 | fmt = "tiff" ; |
182 | break; |
183 | } |
184 | int quality = -1; |
185 | switch (m_settings.quality()) { |
186 | case QImageCapture::VeryLowQuality: |
187 | quality = 25; |
188 | break; |
189 | case QImageCapture::LowQuality: |
190 | quality = 50; |
191 | break; |
192 | case QImageCapture::NormalQuality: |
193 | break; |
194 | case QImageCapture::HighQuality: |
195 | quality = 75; |
196 | break; |
197 | case QImageCapture::VeryHighQuality: |
198 | quality = 99; |
199 | break; |
200 | } |
201 | |
202 | QImageWriter writer(pending.filename, fmt); |
203 | writer.setQuality(quality); |
204 | |
205 | if (writer.write(image)) { |
206 | emit imageSaved(requestId: pending.id, fileName: pending.filename); |
207 | } else { |
208 | QImageCapture::Error err = QImageCapture::ResourceError; |
209 | if (writer.error() == QImageWriter::UnsupportedFormatError) |
210 | err = QImageCapture::FormatError; |
211 | emit error(id: pending.id, error: err, errorString: writer.errorString()); |
212 | } |
213 | } |
214 | |
215 | updateReadyForCapture(); |
216 | } |
217 | |
218 | void QFFmpegImageCapture::setupVideoSourceConnections() |
219 | { |
220 | connect(sender: m_videoSource, signal: &QPlatformCamera::newVideoFrame, context: this, |
221 | slot: &QFFmpegImageCapture::newVideoFrame); |
222 | } |
223 | |
224 | QPlatformVideoSource *QFFmpegImageCapture::videoSource() const |
225 | { |
226 | return m_videoSource; |
227 | } |
228 | |
229 | void QFFmpegImageCapture::onVideoSourceChanged() |
230 | { |
231 | if (m_videoSource) |
232 | m_videoSource->disconnect(receiver: this); |
233 | |
234 | m_videoSource = m_session ? m_session->primaryActiveVideoSource() : nullptr; |
235 | |
236 | // TODO: optimize, setup the connection only when the capture is ready |
237 | if (m_videoSource) |
238 | setupVideoSourceConnections(); |
239 | |
240 | updateReadyForCapture(); |
241 | } |
242 | |
243 | QImageEncoderSettings QFFmpegImageCapture::imageSettings() const |
244 | { |
245 | return m_settings; |
246 | } |
247 | |
248 | void QFFmpegImageCapture::setImageSettings(const QImageEncoderSettings &settings) |
249 | { |
250 | auto s = settings; |
251 | const auto supportedFormats = QPlatformMediaIntegration::instance()->formatInfo()->imageFormats; |
252 | if (supportedFormats.isEmpty()) { |
253 | emit error(id: -1, error: QImageCapture::FormatError, errorString: "No image formats supported, can't capture." ); |
254 | return; |
255 | } |
256 | if (s.format() == QImageCapture::UnspecifiedFormat) { |
257 | auto f = QImageCapture::JPEG; |
258 | if (!supportedFormats.contains(t: f)) |
259 | f = supportedFormats.first(); |
260 | s.setFormat(f); |
261 | } else if (!supportedFormats.contains(t: settings.format())) { |
262 | emit error(id: -1, error: QImageCapture::FormatError, errorString: "Image format not supported." ); |
263 | return; |
264 | } |
265 | |
266 | m_settings = settings; |
267 | } |
268 | |
269 | QT_END_NAMESPACE |
270 | |
271 | #include "moc_qffmpegimagecapture_p.cpp" |
272 | |