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
qavfcamera.mm
Go to the documentation of this file.
1// Copyright (C) 2022 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 <QtFFmpegMediaPluginImpl/private/qavfcamera_p.h>
5
6#include <QtCore/qscopeguard.h>
7#include <QtCore/private/qcore_mac_p.h>
8
9#include <QtFFmpegMediaPluginImpl/private/qavfcapturephotooutputdelegate_p.h>
10#include <QtFFmpegMediaPluginImpl/private/qavfsamplebufferdelegate_p.h>
11#include <QtFFmpegMediaPluginImpl/private/qffmpegdarwinintegrationfactory_p.h>
12
13#include <QtMultimedia/private/qavfcameradebug_p.h>
14#include <QtMultimedia/private/qavfcamerautility_p.h>
15#include <QtMultimedia/private/qavfhelpers_p.h>
16#include <QtMultimedia/private/qmultimediautils_p.h>
17#include <QtMultimedia/private/qplatformmediacapture_p.h>
18
19#define AVMediaType XAVMediaType
20extern "C" {
21#include <libavutil/hwcontext_videotoolbox.h>
22#include <libavutil/hwcontext.h>
23}
24#undef AVMediaType
25
26QT_BEGIN_NAMESPACE
27
28namespace QFFmpeg {
29
30namespace {
31
32[[nodiscard]] AVCaptureFlashMode toAvfFlashMode(QCamera::FlashMode flashMode)
33{
34 switch (flashMode) {
35 case QCamera::FlashMode::FlashOff:
36 return AVCaptureFlashModeOff;
37 case QCamera::FlashMode::FlashAuto:
38 return AVCaptureFlashModeAuto;
39 case QCamera::FlashMode::FlashOn:
40 return AVCaptureFlashModeOn;
41 }
42 return AVCaptureFlashModeOff;
43}
44
45[[nodiscard]] bool checkAvCapturePhotoFormatSupport(AVCapturePhotoOutput *output, int cvPixelFormat)
46{
47 Q_ASSERT(output);
48 NSArray<NSNumber *> *supportedFormats = output.availablePhotoPixelFormatTypes;
49 for (NSNumber *format : supportedFormats) {
50 if (format.intValue == cvPixelFormat)
51 return true;
52 }
53 return false;
54}
55
56[[nodiscard]] QAVFSampleBufferDelegateTransform surfaceTransform(
57 const QFFmpeg::AvfCameraRotationTracker *rotationTracker,
58 const AVCaptureConnection *connection)
59{
60 QAVFSampleBufferDelegateTransform transform = {};
61
62 int captureAngle = 0;
63
64 if (rotationTracker != nullptr) {
65 captureAngle = rotationTracker->rotationDegrees();
66
67 bool cameraIsFrontFacing =
68 rotationTracker->avCaptureDevice() != nullptr
69 && rotationTracker->avCaptureDevice().position == AVCaptureDevicePositionFront;
70 if (cameraIsFrontFacing)
71 transform.presentationTransform.mirroredHorizontallyAfterRotation = true;
72 }
73
74 // In some situations, AVFoundation can set the AVCaptureConnection.videoRotationAgngle
75 // implicity and start rotating the pixel buffer before handing it back
76 // to us. In this case we want to account for this during preview and capture.
77 //
78 // This code assumes that AVCaptureConnection.videoRotationAngle returns degrees
79 // that are divisible by 90. This has been the case during testing.
80 int connectionAngle = 0;
81 if (connection) {
82 if (@available(macOS 14.0, iOS 17.0, *))
83 connectionAngle = std::lround(connection.videoRotationAngle);
84
85 if (connection.videoMirrored)
86 transform.surfaceTransform.mirroredHorizontallyAfterRotation = true;
87 }
88
89 transform.surfaceTransform.rotation = qVideoRotationFromDegrees(captureAngle - connectionAngle);
90
91 return transform;
92}
93
94// This function may return a nullptr if no suitable format was found.
95// The format may not be supported by FFmpeg.
96[[nodiscard]] static AVCaptureDeviceFormat* findSuitableAvCaptureDeviceFormat(
97 AVCaptureDevice *avCaptureDevice,
98 const QCameraFormat &format)
99{
100 Q_ASSERT(avCaptureDevice != nullptr);
101 Q_ASSERT(!format.isNull());
102
103 // First we try to find a device format equivalent to QCameraFormat
104 // that is supported by FFmpeg.
105 AVCaptureDeviceFormat *newDeviceFormat = qt_convert_to_capture_device_format(
106 avCaptureDevice,
107 format,
108 &QFFmpeg::isCVFormatSupported);
109
110 // If we can't find a AVCaptureDeviceFormat supported by FFmpeg,
111 // fall back to one not supported by FFmpeg.
112 if (!newDeviceFormat)
113 newDeviceFormat = qt_convert_to_capture_device_format(avCaptureDevice, format);
114
115 return newDeviceFormat;
116}
117
118[[nodiscard]] static q23::expected<CvPixelFormat, QString> tryFindVideoDataOutputPixelFormat(
119 QVideoFrameFormat::PixelFormat cameraPixelFormat,
120 CvPixelFormat inputCvPixFormat,
121 AVCaptureVideoDataOutput *avCaptureVideoDataOutput)
122{
123 Q_ASSERT(cameraPixelFormat != QVideoFrameFormat::PixelFormat::Format_Invalid);
124 Q_ASSERT(inputCvPixFormat != CvPixelFormatInvalid);
125 Q_ASSERT(avCaptureVideoDataOutput);
126
127 using namespace Qt::Literals::StringLiterals;
128
129 if (avCaptureVideoDataOutput.availableVideoCVPixelFormatTypes.count == 0)
130 return q23::unexpected{
131 u"AVCaptureVideoDataOutput.availableVideoCVPixelFormatTypes is empty"_s };
132
133 auto bestScore = MinAVScore;
134 NSNumber *bestFormat = nullptr;
135 for (NSNumber *cvPixFmtNumber in avCaptureVideoDataOutput.availableVideoCVPixelFormatTypes) {
136 const CvPixelFormat cvPixFmt = [cvPixFmtNumber unsignedIntValue];
137 const QVideoFrameFormat::PixelFormat pixFmt = QAVFHelpers::fromCVPixelFormat(cvPixFmt);
138 if (pixFmt == QVideoFrameFormat::Format_Invalid)
139 continue;
140
141 auto score = DefaultAVScore;
142 if (cvPixFmt == inputCvPixFormat)
143 score += 100;
144 if (pixFmt == cameraPixelFormat)
145 score += 10;
146 // if (cvPixFmt == kCVPixelFormatType_32BGRA)
147 // score += 1;
148
149 // This flag determines priorities of using ffmpeg hw frames or
150 // the exact camera format match.
151 // Maybe configure more, e.g. by some env var?
152 constexpr bool ShouldSuppressNotSupportedByFFmpeg = false;
153
154 if (!isCVFormatSupported(cvPixFmt))
155 score -= ShouldSuppressNotSupportedByFFmpeg ? 100000 : 5;
156
157 if (score > bestScore) {
158 bestScore = score;
159 bestFormat = cvPixFmtNumber;
160 }
161 }
162
163 if (bestScore < DefaultAVScore)
164 qWarning() << "QAVFCamera::tryFindVideoDataOutputPixelFormat: "
165 "Cannot find hw FFmpeg supported cv pix format";
166
167 return [bestFormat unsignedIntValue];
168}
169
170} // Anonymous namespace
171
173{
174 return std::make_unique<QAVFCamera>(parent);
175}
176
179{
182 dispatch_queue_create("qt_camera_queue", DISPATCH_QUEUE_SERIAL) };
183
185
186 // TODO: Handle error where we cannot add AVCapturePhotoOutput to session,
187 // and report back to QImageCapture that we are unable to take a photo.
190}
191
193{
194 using namespace Qt::Literals::StringLiterals;
195
197
199 // Clearing the output will flush jobs on the dispatch queue running on a worker threadpool.
202
203 // If there is currently an on-going still photo capture, we will
204 // automatically discard any future results when this QCamera object
205 // is destroyed and the connection to the QAVFStillPhotoNotifier is
206 // removed. We emit a signal that still-photo capture failed, so
207 // that QImageCapture can cancel any pending still-photo capture jobs.
211 u"Camera object was destroyed before still photo capture was completed"_s);
212 }
213}
214
216{
220 }
221}
222
225{
226 // AVCaptureDeviceInput.deviceInputWithDevice will implicitly ask for permission
227 // and present a dialogue to the end-user.
228 // Permission should only be requested explicitly through QPermission API.
230 Q_ASSERT(avCaptureDevice != nullptr);
233
234 using namespace Qt::Literals::StringLiterals;
235
237
238 NSError* creationError = nullptr;
242 if (creationError != nullptr)
245
247 return q23::unexpected{
248 u"Cannot attach AVCaptureDeviceInput to AVCaptureSession"_s };
249
251
253
254 return {};
255}
256
257// If there is any current delegate, we block the background thread
258// and set the delegate to discard future frames.
260{
264 }
266 // Push a blocking job to the background frame thread,
267 // so we guarantee future frames are discarded. This
268 // causes the frameHandler to be destroyed, and the reference
269 // to this QAVFCamera is cleared.
273 [this]() {
275 });
276
278 }
279}
280
283{
285
286 using namespace Qt::Literals::StringLiterals;
287
289
290 // Setup the delegate object for which we receive video frames.
291 // This is called by the background thread. The frameHandler must
292 // be cleared on the Delegate when destroying the QAVFCamera,
293 // to avoid any remaining enqueued frame-jobs from reading this QAVFCamera
294 // reference.
295 auto frameHandler = [this](QVideoFrame frame) {
298 };
299
303 // The transformProvider callable needs to be copyable, so we use a shared-ptr here.
307 return surfaceTransform(
309 connection);
310 }];
311
312 // Create the AVCaptureOutput object with our delegate object and background-thread.
314 init]
318
320 return q23::unexpected{
321 u"Unable to connect AVCaptureVideoDataOutput to AVCaptureSession"_s };
322
326
327 return {};
328}
329
330// This function writes to the AVCaptureVideoDataOutput and QAVFSampleBufferDelegate
331// objects directly. Don't use this function if these objects are already
332// connected to a running AVCaptureSession.
337{
341
344
345 // We cannot always use the AVCaptureDeviceFormat directly,
346 // so we look for a pixel format that we can use for the output.
347 // The AVFoundation internals will take care of converting the
348 // pixel formats to what we require.
356
358
359 // If the input AVCaptureDevice pixel format does not match
360 // the output pixel format, the AVFoundation internals will perform
361 // the conversion for us. This likely incurs performance overhead.
363 qCWarning(qLcCamera) << "Output CV format differs with capture device format!"
365 << "vs"
367 }
368
370
372
374 qCWarning(qLcCamera) << "Videotoolbox doesn't support cvPixelFormat:" << outputCvPixelFormat
376 << "Camera pix format:" << newCameraFormat.pixelFormat();
377 } else {
379 qCDebug(qLcCamera) << "Create VIDEOTOOLBOX hw context" << hwAccel.get() << "for camera";
380 }
381
382 // Apply the format to our capture session and QAVFCamera.
383
384 if (hwAccel) {
387 } else {
389 }
390
394
400 };
402
404
407
408 return {};
409}
410
412{
414}
415
417{
418 Q_ASSERT(avCaptureDevice != nullptr);
420}
421
423{
427}
428
432{
433 using namespace Qt::Literals::StringLiterals;
434
436 if (avCaptureDevice == nullptr)
437 return q23::unexpected{ u"AVCaptureDevice not available"_s };
438
442}
443
447{
448 using namespace Qt::Literals::StringLiterals;
449
453 // If we can't find any suitable AVCaptureDeviceFormat,
454 // then we cannot apply this QCameraFormat.
455 if (avCaptureDeviceFormat == nullptr)
456 return q23::unexpected{
457 u"Unable to find any suitable AVCaptureDeviceFormat when attempting to "
458 "apply QCameraFormat"_s };
459
464}
465
470{
473
476 if (!setupInputResult)
478
483
490
492
493 return {};
494}
495
497{
498 if (active) {
499 // We should never try to go active if we don't already have
500 // permissions, as refreshAvCaptureSessionInputDevice() will
501 // implicitly trigger a user permission request and freeze the
502 // program. Permissions should only be requested through
503 // QPermissions.
505
507 if (avCaptureDevice == nullptr) {
508 qWarning() << "QAVFCamera::onActiveChanged: Device not available";
509 return;
510 }
511
512 // The AVCaptureDevice must be locked when we call AVCaptureSession.startRunning,
513 // in order to not have the AVCaptureDeviceFormat be overriden by the AVCaptureSession's
514 // quality preset. Additionally, we apply the format inside tryConfigureCaptureSession,
515 // so it's beneficial to keep the device locked during the entire config stage.
517 if (!avCaptureDeviceLock) {
518 qWarning() << "QAVFCamera::onActiveChanged: Failed to lock AVCaptureDevice";
519 return;
520 }
521
524 cameraFormat());
525 if (configureResult) {
527 } else {
528 qWarning()
529 << "QAVFCamera::onActiveChanged: Error when trying to activate camera:"
532 }
533
534 } else {
536
538 }
539}
540
545
549{
550 // The incoming format should never be null if the incoming device is not null.
552
553 // We cannot call AVCaptureSession.stopRunning() inside a
554 // AVCaptureSession configuration scope, so we wrap that scope in
555 // a lambda and call stopRunning() afterwards if configuration
556 // fails for the new QCameraDevice.
557
558 auto tryChangeDeviceFn = [&]() -> q23::expected<void, QString> {
559 // Using this configuration transaction, we can clear up
560 // resources and establish new ones without having to do slow
561 // and synchronous calls to AVCaptureSession.stopRunning and startRunning.
565 } };
566
568
569 // If the new QCameraDevice does not point to any physical device,
570 // make sure we clear resources and shut down the capture-session.
572 return {};
573
574 // If we are not currently active, then we can just accept the new property
575 // value and return.
577 return {};
578
581 newFormat);
582 if (!configureResult) {
584 return configureResult;
585 }
586
587 return {};
588 };
589
591 if (!changeDeviceResult) {
593 qWarning()
594 << "Error when trying to activate new camera-device: "
596 }
597}
598
600{
603
604 // TODO: It's currently unclear whether we should accept the QCameraFormat
605 // if the QCameraDevice is currently not connected.
607 if (!avCaptureDevice)
608 return false;
609
613 // If we can't find any suitable AVCaptureDeviceFormat,
614 // then we cannot apply this QCameraFormat.
616 qWarning() << "QAVFCamera::tryApplyCameraFormat: Unable to find any suitable "
617 "AVCaptureDeviceFormat when attempting to apply QCameraFormat";
618 return false;
619 }
620
621 // If we are not currently active, we don't need to do anything. We will apply the format
622 // to the capture-session when we try to go active later.
623 //
624 // TODO: Determine if the incoming QCameraFormat resolves to the same formats
625 // that we are already using, in which case this function can be a no-op.
627 return true;
628
629 // We are active, so we need to reconfigure the entire capture-session with the
630 // new format.
632 if (!avCaptureDeviceLock) {
633 qWarning() << "QAVFCamera::tryApplyCameraFormat: Failed to lock AVCaptureDevice when "
634 "trying to apply new QCameraFormat.";
635 return false;
636 }
637
639 QScopeGuard endConfigGuard { [this]() {
641 } };
642
644
649 if (!configureResult) {
650 qWarning()
651 << "Error when trying to activate camera with new format: "
653
656
657 return false;
658 }
659
660 return true;
661}
662
664{
665#ifdef Q_OS_MACOS
666 return newFormat.resolution();
667#else
668 // Check, that we have matching dimesnions.
672 return resolution;
673
674 // Either portrait but actually sizes of landscape, or
675 // landscape with dimensions of portrait - not what
676 // sample delegate will report (it depends on videoOrientation set).
681
682 return resolution;
683#endif // Q_OS_MACOS
684}
685
690
697
720
721// Gets the current rotationfor this QAVFCamera.
722// Returns the result in degrees, 0 to 360.
723// Will always return a result that is divisible by 90.
725{
728 else
729 return 0;
730}
731
732// The still photo finishing will be invoked on a background thread not
733// controlled by us, on the QAvfCapturePhotoOutputDelegate object.
734// Without proper synchronization, we can therefore end up in a
735// situation where the callback is invoked after the QAVFCamera object
736// is destroyed. The current approach is to have a thread-safe call that
737// tells the QAvfCameraPhotoOutputDelegate to discard the results.
739{
744 // We must have an AVCaptureDeviceVideoInput hooked up to our AVCaptureSession
745 // in order for the AVCapturePhotoOutput to be populated with correct values.
747
748 using namespace Qt::Literals::StringLiterals;
749
750 // TODO: We can potentially match the current QCameraFormat here,
751 // which might help us save some bandwidth with i.e YUV420
754 qCWarning(qLcCamera) << "Attempted to take a still photo with an AVCapturePhotoOutput that "
755 "does not support output with 32BGRA format.";
756 return q23::unexpected{ u"Internal camera configuration error"_s };
757 }
758
762
763 // Set the settings for this capture.
764 //
765 // TODO: In the future we should try to respect the size set by QImageCapture here.
766 // For now, we use the same size as whatever the AVCaptureDevice is currently using.
769
772
775
776 // If we mistakenly use settings that are not supported, captureWithSettings will
777 // throw an exception.
778 @try {
781 }
782 @catch (NSException *exception) {
784 u"Attempted to start still photo capture with "
785 "capture-settings that are not supported by AVCapturePhotoOutput: '%1'"_s
788
789 return q23::unexpected{ u"Internal camera configuration error"_s };
790 }
791 @finally {}
792
796 this,
801 this,
803
805
806 return {};
807}
808
810{
814}
815
817{
821}
822
823} // namespace QFFmpeg
824
825QT_END_NAMESPACE
826
827#include "moc_qavfcamera_p.cpp"
std::unique_ptr< QPlatformCamera > makeQAvfCamera(QCamera &parent)
std::conditional_t< QT_FFMPEG_AVIO_WRITE_CONST, const uint8_t *, uint8_t * > AvioWriteBufferType