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