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