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
avfimagecapture.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
8#include "avfcamera_p.h"
11#include "private/qmediastoragelocation_p.h"
12#include <private/qplatformimagecapture_p.h>
13#include <private/qmemoryvideobuffer_p.h>
14#include <private/qvideoframe_p.h>
15
16#include <QtCore/qurl.h>
17#include <QtCore/qfile.h>
18#include <QtCore/qbuffer.h>
19#include <QtConcurrent/qtconcurrentrun.h>
20#include <QtGui/qimagereader.h>
21
22#import <AVFoundation/AVFoundation.h>
23
25
28{
29 m_stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
30
31 NSDictionary *outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:
32 AVVideoCodecTypeJPEG, AVVideoCodecKey, nil];
33
34 [m_stillImageOutput setOutputSettings:outputSettings];
35 [outputSettings release];
36}
37
38AVFImageCapture::~AVFImageCapture()
39{
40 [m_stillImageOutput release];
41}
42
44{
45 return m_cameraControl && m_videoConnection && m_cameraControl->isActive();
46}
47
48void AVFImageCapture::updateReadyStatus()
49{
50 if (m_ready != isReadyForCapture()) {
51 m_ready = !m_ready;
52 qCDebug(qLcCamera) << "ReadyToCapture status changed:" << m_ready;
53 Q_EMIT readyForCaptureChanged(m_ready);
54 }
55}
56
57int AVFImageCapture::doCapture(const QString &actualFileName)
58{
59 if (!m_session) {
60 QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
61 Q_ARG(int, m_lastCaptureId),
62 Q_ARG(int, QImageCapture::ResourceError),
63 Q_ARG(QString, QPlatformImageCapture::msgImageCaptureNotSet()));
64 return -1;
65 }
67 QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
68 Q_ARG(int, m_lastCaptureId),
69 Q_ARG(int, QImageCapture::NotReadyError),
70 Q_ARG(QString, QPlatformImageCapture::msgCameraNotReady()));
71 return -1;
72 }
73 m_lastCaptureId++;
74
75 bool captureToBuffer = actualFileName.isEmpty();
76
77 CaptureRequest request = { m_lastCaptureId, QSharedPointer<QSemaphore>::create()};
78 m_requestsMutex.lock();
79 m_captureRequests.enqueue(request);
80 m_requestsMutex.unlock();
81
82 QString fileName(actualFileName);
83
84 [m_stillImageOutput captureStillImageAsynchronouslyFromConnection:m_videoConnection
85 completionHandler: ^(CMSampleBufferRef imageSampleBuffer, NSError *error) {
86
87 if (error) {
88 QStringList messageParts;
89 messageParts << QString::fromUtf8([[error localizedDescription] UTF8String]);
90 messageParts << QString::fromUtf8([[error localizedFailureReason] UTF8String]);
91 messageParts << QString::fromUtf8([[error localizedRecoverySuggestion] UTF8String]);
92
93 QString errorMessage = messageParts.join(QChar(u' '));
94 qCDebug(qLcCamera) << "Image capture failed:" << errorMessage;
95
96 QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
97 Q_ARG(int, request.captureId),
98 Q_ARG(int, QImageCapture::ResourceError),
99 Q_ARG(QString, errorMessage));
100 return;
101 }
102
103 // Wait for the preview to be generated before saving the JPEG (but only
104 // if we have AVFCameraRenderer attached).
105 // It is possible to stop camera immediately after trying to capture an
106 // image; this can result in a blocked callback's thread, waiting for a
107 // new viewfinder's frame to arrive/semaphore to be released. It is also
108 // unspecified on which thread this callback gets executed, (probably it's
109 // not the same thread that initiated a capture and stopped the camera),
110 // so we cannot reliably check the camera's status. Instead, we wait
111 // with a timeout and treat a failure to acquire a semaphore as an error.
112 if (!m_session->videoOutput() || request.previewReady->tryAcquire(1, 1000)) {
113 qCDebug(qLcCamera) << "Image capture completed";
114
115 NSData *nsJpgData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer];
116 QByteArray jpgData = QByteArray::fromRawData((const char *)[nsJpgData bytes], [nsJpgData length]);
117
118 if (captureToBuffer) {
119 QBuffer data(&jpgData);
120 QImageReader reader(&data, "JPEG");
121 QSize size = reader.size();
122 auto buffer = std::make_unique<QMemoryVideoBuffer>(
123 QByteArray(jpgData.constData(), jpgData.size()), -1);
124 QVideoFrame frame = QVideoFramePrivate::createFrame(
125 std::move(buffer), QVideoFrameFormat(size, QVideoFrameFormat::Format_Jpeg));
126 QMetaObject::invokeMethod(this, "imageAvailable", Qt::QueuedConnection,
127 Q_ARG(int, request.captureId),
128 Q_ARG(QVideoFrame, frame));
129 } else {
130 QFile f(fileName);
131 if (f.open(QFile::WriteOnly)) {
132 if (f.write(jpgData) != -1) {
133 QMetaObject::invokeMethod(this, "imageSaved", Qt::QueuedConnection,
134 Q_ARG(int, request.captureId),
135 Q_ARG(QString, fileName));
136 } else {
137 QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
138 Q_ARG(int, request.captureId),
139 Q_ARG(int, QImageCapture::OutOfSpaceError),
140 Q_ARG(QString, f.errorString()));
141 }
142 } else {
143 QString errorMessage = tr("Could not open destination file:\n%1").arg(fileName);
144 QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
145 Q_ARG(int, request.captureId),
146 Q_ARG(int, QImageCapture::ResourceError),
147 Q_ARG(QString, errorMessage));
148 }
149 }
150 } else {
151 const QLatin1String errorMessage("Image capture failed: timed out waiting"
152 " for a preview frame.");
153 qCDebug(qLcCamera) << errorMessage;
154 QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection,
155 Q_ARG(int, request.captureId),
156 Q_ARG(int, QImageCapture::ResourceError),
157 Q_ARG(QString, errorMessage));
158 }
159 }];
160
161 return request.captureId;
162}
163
164int AVFImageCapture::capture(const QString &fileName)
165{
166 auto actualFileName = QMediaStorageLocation::generateFileName(fileName, QStandardPaths::PicturesLocation, QLatin1String("jpg"));
167
168 qCDebug(qLcCamera) << "Capture image to" << actualFileName;
169 return doCapture(actualFileName);
170}
171
173{
174 return doCapture(QString());
175}
176
177void AVFImageCapture::onNewViewfinderFrame(const QVideoFrame &frame)
178{
179 QMutexLocker locker(&m_requestsMutex);
180
181 if (m_captureRequests.isEmpty())
182 return;
183
184 CaptureRequest request = m_captureRequests.dequeue();
185 Q_EMIT imageExposed(request.captureId);
186
187 (void) QtConcurrent::run(&AVFImageCapture::makeCapturePreview, this,
188 request,
189 frame,
190 0 /* rotation */);
191}
192
193void AVFImageCapture::onCameraChanged()
194{
195 auto camera = m_service ? static_cast<AVFCamera *>(m_service->camera()) : nullptr;
196
197 if (camera == m_cameraControl)
198 return;
199
200 m_cameraControl = camera;
201
202 if (m_cameraControl)
203 connect(m_cameraControl, SIGNAL(activeChanged(bool)), this, SLOT(updateReadyStatus()));
204 updateReadyStatus();
205}
206
207void AVFImageCapture::makeCapturePreview(CaptureRequest request,
208 const QVideoFrame &frame,
209 int rotation)
210{
211 QTransform transform;
212 transform.rotate(rotation);
213
214 Q_EMIT imageCaptured(request.captureId, frame.toImage().transformed(transform));
215
216 request.previewReady->release();
217}
218
219void AVFImageCapture::updateCaptureConnection()
220{
221 if (m_session && m_session->videoCaptureDevice()) {
222 qCDebug(qLcCamera) << Q_FUNC_INFO;
223 AVCaptureSession *captureSession = m_session->captureSession();
224
225 if (![captureSession.outputs containsObject:m_stillImageOutput]) {
226 if ([captureSession canAddOutput:m_stillImageOutput]) {
227 [captureSession beginConfiguration];
228 // Lock the video capture device to make sure the active format is not reset
229 const AVFConfigurationLock lock(m_session->videoCaptureDevice());
230 [captureSession addOutput:m_stillImageOutput];
231 m_videoConnection = [m_stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
232 [captureSession commitConfiguration];
233 updateReadyStatus();
234 }
235 } else {
236 m_videoConnection = [m_stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
237 }
238 }
239}
240
241
243{
244 QImageEncoderSettings settings;
245
246 if (!videoCaptureDeviceIsValid())
247 return settings;
248
249 AVCaptureDevice *captureDevice = m_service->session()->videoCaptureDevice();
250 if (!captureDevice.activeFormat) {
251 qCDebug(qLcCamera) << Q_FUNC_INFO << "no active format";
252 return settings;
253 }
254
255 QSize res(qt_device_format_resolution(captureDevice.activeFormat));
256#ifdef Q_OS_IOS
257 if (!m_service->avfImageCaptureControl() || !m_service->avfImageCaptureControl()->stillImageOutput()) {
258 qCDebug(qLcCamera) << Q_FUNC_INFO << "no still image output";
259 return settings;
260 }
261
262 AVCaptureStillImageOutput *stillImageOutput = m_service->avfImageCaptureControl()->stillImageOutput();
263 if (stillImageOutput.highResolutionStillImageOutputEnabled)
264 res = qt_device_format_high_resolution(captureDevice.activeFormat);
265#endif
266 if (res.isNull() || !res.isValid()) {
267 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to exctract the image resolution";
268 return settings;
269 }
270
271 settings.setResolution(res);
272 settings.setFormat(QImageCapture::JPEG);
273
274 return settings;
275}
276
277void AVFImageCapture::setImageSettings(const QImageEncoderSettings &settings)
278{
279 if (m_settings == settings)
280 return;
281
282 m_settings = settings;
284}
285
287{
288 if (!videoCaptureDeviceIsValid())
289 return false;
290
291 AVFCameraSession *session = m_service->session();
292 if (!session)
293 return false;
294
295 if (!m_service->imageCapture()
296 || !m_service->avfImageCaptureControl()->stillImageOutput()) {
297 qCDebug(qLcCamera) << Q_FUNC_INFO << "no still image output";
298 return false;
299 }
300
301 if (m_settings.format() != QImageCapture::UnspecifiedFormat && m_settings.format() != QImageCapture::JPEG) {
302 qCDebug(qLcCamera) << Q_FUNC_INFO << "unsupported format:" << m_settings.format();
303 return false;
304 }
305
306 QSize res(m_settings.resolution());
307 if (res.isNull()) {
308 qCDebug(qLcCamera) << Q_FUNC_INFO << "invalid resolution:" << res;
309 return false;
310 }
311
312 if (!res.isValid()) {
313 // Invalid == default value.
314 // Here we could choose the best format available, but
315 // activeFormat is already equal to 'preset high' by default,
316 // which is good enough, otherwise we can end in some format with low framerates.
317 return false;
318 }
319
320 bool activeFormatChanged = false;
321
322 AVCaptureDevice *captureDevice = m_service->session()->videoCaptureDevice();
323 AVCaptureDeviceFormat *match = qt_find_best_resolution_match(captureDevice, res,
324 m_service->session()->defaultCodec());
325
326 if (!match) {
327 qCDebug(qLcCamera) << Q_FUNC_INFO << "unsupported resolution:" << res;
328 return false;
329 }
330
331 activeFormatChanged = qt_set_active_format(captureDevice, match, true);
332
333#ifdef Q_OS_IOS
334 AVCaptureStillImageOutput *imageOutput = m_service->avfImageCaptureControl()->stillImageOutput();
335 if (res == qt_device_format_high_resolution(captureDevice.activeFormat))
336 imageOutput.highResolutionStillImageOutputEnabled = YES;
337 else
338 imageOutput.highResolutionStillImageOutputEnabled = NO;
339#endif
340
341 return activeFormatChanged;
342}
343
344void AVFImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session)
345{
346 AVFCameraService *captureSession = static_cast<AVFCameraService *>(session);
347 if (m_service == captureSession)
348 return;
349
350 m_service = captureSession;
351 if (!m_service) {
352 m_session->disconnect(this);
353 if (m_cameraControl)
354 m_cameraControl->disconnect(this);
355 m_session = nullptr;
356 m_cameraControl = nullptr;
357 m_videoConnection = nil;
358 } else {
359 m_session = m_service->session();
360 Q_ASSERT(m_session);
361
362 connect(m_service, &AVFCameraService::cameraChanged, this, &AVFImageCapture::onCameraChanged);
363 connect(m_session, SIGNAL(readyToConfigureConnections()), SLOT(updateCaptureConnection()));
364 connect(m_session, &AVFCameraSession::newViewfinderFrame,
365 this, &AVFImageCapture::onNewViewfinderFrame);
366 }
367
368 updateCaptureConnection();
369 onCameraChanged();
370 updateReadyStatus();
371}
372
373bool AVFImageCapture::videoCaptureDeviceIsValid() const
374{
375 if (!m_service || !m_service->session() || !m_service->session()->videoCaptureDevice())
376 return false;
377
378 AVCaptureDevice *captureDevice = m_service->session()->videoCaptureDevice();
379 if (!captureDevice.formats || !captureDevice.formats.count)
380 return false;
381
382 return true;
383}
384
385#include "moc_avfimagecapture_p.cpp"
AVFCameraSession * session() const
AVFImageCapture * avfImageCaptureControl() const
int doCapture(const QString &fileName)
void setCaptureSession(QPlatformMediaCaptureSession *session)
int captureToBuffer() override
QImageEncoderSettings imageSettings() const override
int capture(const QString &fileName) override
bool isReadyForCapture() const override
void setImageSettings(const QImageEncoderSettings &settings) override
\inmodule QtMultimedia
QObject * parent
Definition qobject.h:73