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
27using namespace Qt::Literals;
28
29AVFImageCapture::AVFImageCapture(QImageCapture *parent)
30 : QPlatformImageCapture(parent)
31{
32 m_stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
33
34 NSDictionary *outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:
35 AVVideoCodecTypeJPEG, AVVideoCodecKey, nil];
36
37 [m_stillImageOutput setOutputSettings:outputSettings];
38 [outputSettings release];
39}
40
41AVFImageCapture::~AVFImageCapture()
42{
43 [m_stillImageOutput release];
44}
45
46bool AVFImageCapture::isReadyForCapture() const
47{
48 return m_cameraControl && m_videoConnection && m_cameraControl->isActive();
49}
50
51void AVFImageCapture::updateReadyStatus()
52{
53 if (m_ready != isReadyForCapture()) {
54 m_ready = !m_ready;
55 qCDebug(qLcCamera) << "ReadyToCapture status changed:" << m_ready;
56 Q_EMIT readyForCaptureChanged(m_ready);
57 }
58}
59
60int AVFImageCapture::doCapture(const QString &fileName)
61{
62 if (!m_session) {
63 QMetaObject::invokeMethod(this, [this, id = m_lastCaptureId] {
64 error(id, QImageCapture::ResourceError, QPlatformImageCapture::msgImageCaptureNotSet());
65 }, Qt::QueuedConnection);
66 return -1;
67 }
68 if (!isReadyForCapture()) {
69 QMetaObject::invokeMethod(this, [this, id = m_lastCaptureId] {
70 error(id, QImageCapture::NotReadyError, QPlatformImageCapture::msgCameraNotReady());
71 }, Qt::QueuedConnection);
72 return -1;
73 }
74 m_lastCaptureId++;
75
76 bool captureToBuffer = fileName.isEmpty();
77
78 auto request = CaptureRequest{
79 m_lastCaptureId,
80 std::make_unique<QSemaphore>(),
81 };
82 m_requestsMutex.lock();
83 m_captureRequests.push(request);
84 m_requestsMutex.unlock();
85
86 [m_stillImageOutput
87 captureStillImageAsynchronouslyFromConnection:m_videoConnection
88 completionHandler:[=, this](CMSampleBufferRef imageSampleBuffer,
89 NSError *nsError) {
90 if (nsError) {
91 QStringList messageParts{
92 QString::fromNSString(nsError.localizedDescription),
93 QString::fromNSString(nsError.localizedFailureReason),
94 QString::fromNSString(nsError.localizedRecoverySuggestion),
95 };
96
97 QString errorMessage = messageParts.join(QChar(u' '));
98 qCDebug(qLcCamera) << "Image capture failed:" << errorMessage;
99
100 QMetaObject::invokeMethod(this, [this, captureId = request.captureId, errorMessage] {
101 error(captureId, QImageCapture::ResourceError, errorMessage);
102 }, Qt::QueuedConnection);
103 return;
104 }
105
106 // Wait for the preview to be generated before saving the JPEG (but only
107 // if we have AVFCameraRenderer attached).
108 // It is possible to stop camera immediately after trying to capture an
109 // image; this can result in a blocked callback's thread, waiting for a
110 // new viewfinder's frame to arrive/semaphore to be released. It is also
111 // unspecified on which thread this callback gets executed, (probably it's
112 // not the same thread that initiated a capture and stopped the camera),
113 // so we cannot reliably check the camera's status. Instead, we wait
114 // with a timeout and treat a failure to acquire a semaphore as an error.
115 if (!m_session->videoOutput() || request.previewReady->tryAcquire(1, 1000)) {
116 qCDebug(qLcCamera) << "Image capture completed";
117
118 NSData *nsJpgData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer];
119 QByteArray jpgData = QByteArray::fromRawData((const char *)[nsJpgData bytes], [nsJpgData length]);
120
121 if (captureToBuffer) {
122 QBuffer data(&jpgData);
123 QImageReader reader(&data, "JPEG");
124 QSize size = reader.size();
125 auto buffer = std::make_unique<QMemoryVideoBuffer>(
126 QByteArray(jpgData.constData(), jpgData.size()), -1);
127 QVideoFrame frame = QVideoFramePrivate::createFrame(
128 std::move(buffer), QVideoFrameFormat(size, QVideoFrameFormat::Format_Jpeg));
129 QMetaObject::invokeMethod(this, [this, captureId = request.captureId, frame] {
130 imageAvailable(captureId, frame);
131 }, Qt::QueuedConnection);
132 } else {
133 QFile f(fileName);
134 if (f.open(QFile::WriteOnly)) {
135 if (f.write(jpgData) != -1) {
136 QMetaObject::invokeMethod(this,
137 [this, captureId = request.captureId, fileName] {
138 imageSaved(captureId, fileName);
139 }, Qt::QueuedConnection);
140 } else {
141 QString errorString = f.errorString();
142 QMetaObject::invokeMethod(
143 this, [this, captureId = request.captureId, errorString] {
144 error(captureId, QImageCapture::OutOfSpaceError, errorString);
145 }, Qt::QueuedConnection);
146 }
147 } else {
148 QString errorMessage = tr("Could not open destination file:\n%1").arg(fileName);
149 QMetaObject::invokeMethod(this,
150 [this, captureId = request.captureId, errorMessage] {
151 error(captureId, QImageCapture::ResourceError, errorMessage);
152 }, Qt::QueuedConnection);
153 }
154 }
155 } else {
156 const QString errorMessage = u"Image capture failed: timed out waiting"
157 " for a preview frame."_s;
158 qCDebug(qLcCamera) << errorMessage;
159 QMetaObject::invokeMethod(this, [this, captureId = request.captureId, errorMessage] {
160 error(captureId, QImageCapture::ResourceError, errorMessage);
161 }, Qt::QueuedConnection);
162 }
163 }];
164
165 return request.captureId;
166}
167
168int AVFImageCapture::capture(const QString &fileName)
169{
170 auto actualFileName = QMediaStorageLocation::generateFileName(fileName, QStandardPaths::PicturesLocation, QLatin1String("jpg"));
171
172 qCDebug(qLcCamera) << "Capture image to" << actualFileName;
173 return doCapture(actualFileName);
174}
175
176int AVFImageCapture::captureToBuffer()
177{
178 return doCapture(QString());
179}
180
181void AVFImageCapture::onNewViewfinderFrame(const QVideoFrame &frame)
182{
183 std::unique_lock guard(m_requestsMutex);
184
185 if (m_captureRequests.empty())
186 return;
187
188 CaptureRequest request = std::move(m_captureRequests.front());
189 m_captureRequests.pop();
190 guard.unlock();
191
192 Q_EMIT imageExposed(request.captureId);
193
194 QFuture<QImage> future = QtConcurrent::run([frame]() -> QImage {
195 return frame.toImage();
196 });
197
198 future.then(this, [this, request](QImage image) {
199 Q_EMIT imageCaptured(request.captureId, image);
200 request.previewReady->release();
201 });
202}
203
204void AVFImageCapture::onCameraChanged()
205{
206 auto camera = m_service ? static_cast<AVFCamera *>(m_service->camera()) : nullptr;
207
208 if (camera == m_cameraControl)
209 return;
210
211 m_cameraControl = camera;
212
213 if (m_cameraControl)
214 connect(m_cameraControl, &AVFCamera::activeChanged, this,
215 &AVFImageCapture::updateReadyStatus);
216 updateReadyStatus();
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
242QImageEncoderSettings AVFImageCapture::imageSettings() const
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;
283 applySettings();
284}
285
286bool AVFImageCapture::applySettings()
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, &AVFCameraSession::readyToConfigureConnections, this, &AVFImageCapture::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