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/qavfcamerafactory_p.h>
10#include <QtFFmpegMediaPluginImpl/private/qavfcapturephotooutputdelegate_p.h>
11#include <QtFFmpegMediaPluginImpl/private/qavfsamplebufferdelegate_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
27
28QT_BEGIN_NAMESPACE
29
30namespace QFFmpeg {
31
32namespace {
33
34[[nodiscard]] AVCaptureFlashMode toAvfFlashMode(QCamera::FlashMode flashMode)
35{
36 switch (flashMode) {
37 case QCamera::FlashMode::FlashOff:
38 return AVCaptureFlashModeOff;
39 case QCamera::FlashMode::FlashAuto:
40 return AVCaptureFlashModeAuto;
41 case QCamera::FlashMode::FlashOn:
42 return AVCaptureFlashModeOn;
43 }
44 return AVCaptureFlashModeOff;
45}
46
47[[nodiscard]] bool checkAvCapturePhotoFormatSupport(AVCapturePhotoOutput *output, int cvPixelFormat)
48{
49 Q_ASSERT(output);
50 NSArray<NSNumber *> *supportedFormats = output.availablePhotoPixelFormatTypes;
51 for (NSNumber *format : supportedFormats) {
52 if (format.intValue == cvPixelFormat)
53 return true;
54 }
55 return false;
56}
57
58[[nodiscard]] QAVFSampleBufferDelegateTransform surfaceTransform(
59 const QFFmpeg::AvfCameraRotationTracker *rotationTracker,
60 const AVCaptureConnection *connection)
61{
62 QAVFSampleBufferDelegateTransform transform = {};
63
64 int captureAngle = 0;
65
66 if (rotationTracker != nullptr) {
67 captureAngle = rotationTracker->rotationDegrees();
68
69 bool cameraIsFrontFacing =
70 rotationTracker->avCaptureDevice() != nullptr
71 && rotationTracker->avCaptureDevice().position == AVCaptureDevicePositionFront;
72 if (cameraIsFrontFacing)
73 transform.presentationTransform.mirroredHorizontallyAfterRotation = true;
74 }
75
76 // In some situations, AVFoundation can set the AVCaptureConnection.videoRotationAgngle
77 // implicity and start rotating the pixel buffer before handing it back
78 // to us. In this case we want to account for this during preview and capture.
79 //
80 // This code assumes that AVCaptureConnection.videoRotationAngle returns degrees
81 // that are divisible by 90. This has been the case during testing.
82 int connectionAngle = 0;
83 if (connection) {
84 if (@available(macOS 14.0, iOS 17.0, *))
85 connectionAngle = std::lround(connection.videoRotationAngle);
86
87 if (connection.videoMirrored)
88 transform.surfaceTransform.mirroredHorizontallyAfterRotation = true;
89 }
90
91 transform.surfaceTransform.rotation = qVideoRotationFromDegrees(captureAngle - connectionAngle);
92
93 return transform;
94}
95
96// This function may return a nullptr if no suitable format was found.
97// The format may not be supported by FFmpeg.
98[[nodiscard]] static AVCaptureDeviceFormat* findSuitableAvCaptureDeviceFormat(
99 AVCaptureDevice *avCaptureDevice,
100 const QCameraFormat &format)
101{
102 Q_ASSERT(avCaptureDevice != nullptr);
103 Q_ASSERT(!format.isNull());
104
105 // First we try to find a device format equivalent to QCameraFormat
106 // that is supported by FFmpeg.
107 AVCaptureDeviceFormat *newDeviceFormat = qt_convert_to_capture_device_format(
108 avCaptureDevice,
109 format,
110 &QFFmpeg::isCVFormatSupported);
111
112 // If we can't find a AVCaptureDeviceFormat supported by FFmpeg,
113 // fall back to one not supported by FFmpeg.
114 if (!newDeviceFormat)
115 newDeviceFormat = qt_convert_to_capture_device_format(avCaptureDevice, format);
116
117 return newDeviceFormat;
118}
119
120[[nodiscard]] static q23::expected<CvPixelFormat, QString> tryFindVideoDataOutputPixelFormat(
121 QVideoFrameFormat::PixelFormat cameraPixelFormat,
122 CvPixelFormat inputCvPixFormat,
123 AVCaptureVideoDataOutput *avCaptureVideoDataOutput)
124{
125 Q_ASSERT(cameraPixelFormat != QVideoFrameFormat::PixelFormat::Format_Invalid);
126 Q_ASSERT(inputCvPixFormat != CvPixelFormatInvalid);
127 Q_ASSERT(avCaptureVideoDataOutput != nullptr);
128
129 using namespace Qt::Literals::StringLiterals;
130
131 if (avCaptureVideoDataOutput.availableVideoCVPixelFormatTypes.count == 0)
132 return q23::unexpected{
133 u"AVCaptureVideoDataOutput.availableVideoCVPixelFormatTypes is empty"_s };
134
135 auto bestScore = MinAVScore;
136 NSNumber *bestFormat = nullptr;
137 for (NSNumber *cvPixFmtNumber in avCaptureVideoDataOutput.availableVideoCVPixelFormatTypes) {
138 const CvPixelFormat cvPixFmt = [cvPixFmtNumber unsignedIntValue];
139 const QVideoFrameFormat::PixelFormat pixFmt = QAVFHelpers::fromCVPixelFormat(cvPixFmt);
140 if (pixFmt == QVideoFrameFormat::Format_Invalid)
141 continue;
142
143 auto score = DefaultAVScore;
144 if (cvPixFmt == inputCvPixFormat)
145 score += 100;
146 if (pixFmt == cameraPixelFormat)
147 score += 10;
148 // if (cvPixFmt == kCVPixelFormatType_32BGRA)
149 // score += 1;
150
151 // This flag determines priorities of using ffmpeg hw frames or
152 // the exact camera format match.
153 // Maybe configure more, e.g. by some env var?
154 constexpr bool ShouldSuppressNotSupportedByFFmpeg = false;
155
156 if (!isCVFormatSupported(cvPixFmt))
157 score -= ShouldSuppressNotSupportedByFFmpeg ? 100000 : 5;
158
159 if (score > bestScore) {
160 bestScore = score;
161 bestFormat = cvPixFmtNumber;
162 }
163 }
164
165 if (bestScore < DefaultAVScore)
166 qWarning() << "QAVFCamera::tryFindVideoDataOutputPixelFormat: "
167 "Cannot find hw FFmpeg supported cv pix format";
168
169 return [bestFormat unsignedIntValue];
170}
171
172} // Anonymous namespace
173
175{
176 return std::make_unique<QAVFCamera>(parent);
177}
178
181{
184 dispatch_queue_create("qt_camera_queue", DISPATCH_QUEUE_SERIAL) };
185
187
188 // TODO: Handle error where we cannot add AVCapturePhotoOutput to session,
189 // and report back to QImageCapture that we are unable to take a photo.
192}
193
195{
196 using namespace Qt::Literals::StringLiterals;
197
199
201 // Clearing the output will flush jobs on the dispatch queue running on a worker threadpool.
204
205 // If there is currently an on-going still photo capture, we will
206 // automatically discard any future results when this QCamera object
207 // is destroyed and the connection to the QAVFStillPhotoNotifier is
208 // removed. We emit a signal that still-photo capture failed, so
209 // that QImageCapture can cancel any pending still-photo capture jobs.
213 u"Camera object was destroyed before still photo capture was completed"_s);
214 }
215
217}
218
220{
225 }
226}
227
230{
231 // AVCaptureDeviceInput.deviceInputWithDevice will implicitly ask for permission
232 // and present a dialogue to the end-user.
233 // Permission should only be requested explicitly through QPermission API.
235 Q_ASSERT(avCaptureDevice != nullptr);
236 Q_ASSERT(m_avCaptureSession != nullptr);
238
239 using namespace Qt::Literals::StringLiterals;
240
242
243 NSError* creationError = nullptr;
247 if (creationError != nullptr)
249
251 return q23::unexpected{
252 u"Cannot attach AVCaptureDeviceInput to AVCaptureSession"_s };
253
255
257
259
260 return {};
261}
262
263// If there is any current delegate, we block the background thread
264// and set the delegate to discard future frames.
266{
267 if (m_avCaptureVideoDataOutput != nullptr) {
271 }
272 if (m_qAvfSampleBufferDelegate != nullptr) {
273 // Push a blocking job to the background frame thread,
274 // so we guarantee future frames are discarded. This
275 // causes the frameHandler to be destroyed, and the reference
276 // to this QAVFCamera is cleared.
280 [this]() {
282 });
283
286 }
287}
288
291{
293
294 using namespace Qt::Literals::StringLiterals;
295
297
298 // Setup the delegate object for which we receive video frames.
299 // This is called by the background thread. The frameHandler must
300 // be cleared on the Delegate when destroying the QAVFCamera,
301 // to avoid any remaining enqueued frame-jobs from reading this QAVFCamera
302 // reference.
303 auto frameHandler = [this](QVideoFrame frame) {
306 };
307
311 // The transformProvider callable needs to be copyable, so we use a shared-ptr here.
315 return surfaceTransform(
317 connection);
318 }];
319
320 // Create the AVCaptureOutput object with our delegate object and background-thread.
322 init]
326
328 return q23::unexpected{
329 u"Unable to connect AVCaptureVideoDataOutput to AVCaptureSession"_s };
330
334
335 return {};
336}
337
338// This function writes to the AVCaptureVideoDataOutput and QAVFSampleBufferDelegate
339// objects directly. Don't use this function if these objects are already
340// connected to a running AVCaptureSession.
345{
346 Q_ASSERT(avCaptureDevice != nullptr);
349
352
353 // We cannot always use the AVCaptureDeviceFormat directly,
354 // so we look for a pixel format that we can use for the output.
355 // The AVFoundation internals will take care of converting the
356 // pixel formats to what we require.
364
366
367 // If the input AVCaptureDevice pixel format does not match
368 // the output pixel format, the AVFoundation internals will perform
369 // the conversion for us. This likely incurs performance overhead.
371 qCWarning(qLcCamera) << "Output CV format differs with capture device format!"
373 << "vs"
375 }
376
378
380
382 qCWarning(qLcCamera) << "Videotoolbox doesn't support cvPixelFormat:" << outputCvPixelFormat
384 << "Camera pix format:" << newCameraFormat.pixelFormat();
385 } else {
387 qCDebug(qLcCamera) << "Create VIDEOTOOLBOX hw context" << hwAccel.get() << "for camera";
388 }
389
390 // Apply the format to our capture session and QAVFCamera.
391
392 if (hwAccel) {
395 } else {
397 }
398
402
408 };
410
412
415
416 return {};
417}
418
420{
422}
423
425{
426 Q_ASSERT(avCaptureDevice != nullptr);
428}
429
431{
435}
436
440{
441 using namespace Qt::Literals::StringLiterals;
442
444 if (avCaptureDevice == nullptr)
445 return q23::unexpected{ u"AVCaptureDevice not available"_s };
446
450}
451
455{
456 using namespace Qt::Literals::StringLiterals;
457
461 // If we can't find any suitable AVCaptureDeviceFormat,
462 // then we cannot apply this QCameraFormat.
463 if (avCaptureDeviceFormat == nullptr)
464 return q23::unexpected{
465 u"Unable to find any suitable AVCaptureDeviceFormat when attempting to "
466 "apply QCameraFormat"_s };
467
472}
473
478{
479 Q_ASSERT(avCaptureDevice != nullptr);
481
484 if (!setupInputResult)
486
491
498
500
501 return {};
502}
503
505{
506 if (active) {
507 // We should never try to go active if we don't already have
508 // permissions, as refreshAvCaptureSessionInputDevice() will
509 // implicitly trigger a user permission request and freeze the
510 // program. Permissions should only be requested through
511 // QPermissions.
513
515 if (avCaptureDevice == nullptr) {
516 qWarning() << "QAVFCamera::onActiveChanged: Device not available";
517 return;
518 }
519
520 // The AVCaptureDevice must be locked when we call AVCaptureSession.startRunning,
521 // in order to not have the AVCaptureDeviceFormat be overriden by the AVCaptureSession's
522 // quality preset. Additionally, we apply the format inside tryConfigureCaptureSession,
523 // so it's beneficial to keep the device locked during the entire config stage.
525 if (!avCaptureDeviceLock) {
526 qWarning() << "QAVFCamera::onActiveChanged: Failed to lock AVCaptureDevice";
527 return;
528 }
529
532 cameraFormat());
533 if (configureResult) {
535 } else {
536 qWarning()
537 << "QAVFCamera::onActiveChanged: Error when trying to activate camera:"
540 }
541
542 } else {
544
546 }
547}
548
553
557{
558 // The incoming format should never be null if the incoming device is not null.
560
561 // We cannot call AVCaptureSession.stopRunning() inside a
562 // AVCaptureSession configuration scope, so we wrap that scope in
563 // a lambda and call stopRunning() afterwards if configuration
564 // fails for the new QCameraDevice.
565
566 auto tryChangeDeviceFn = [&]() -> q23::expected<void, QString> {
567 // Using this configuration transaction, we can clear up
568 // resources and establish new ones without having to do slow
569 // and synchronous calls to AVCaptureSession.stopRunning and startRunning.
573 } };
574
576
577 // If the new QCameraDevice does not point to any physical device,
578 // make sure we clear resources and shut down the capture-session.
580 return {};
581
582 // If we are not currently active, then we can just accept the new property
583 // value and return.
585 return {};
586
589 newFormat);
590 if (!configureResult) {
592 return configureResult;
593 }
594
595 return {};
596 };
597
599 if (!changeDeviceResult) {
601 qWarning()
602 << "Error when trying to activate new camera-device: "
604 }
605}
606
608{
611
612 // TODO: It's currently unclear whether we should accept the QCameraFormat
613 // if the QCameraDevice is currently not connected.
615 if (!avCaptureDevice)
616 return false;
617
621 // If we can't find any suitable AVCaptureDeviceFormat,
622 // then we cannot apply this QCameraFormat.
624 qWarning() << "QAVFCamera::tryApplyCameraFormat: Unable to find any suitable "
625 "AVCaptureDeviceFormat when attempting to apply QCameraFormat";
626 return false;
627 }
628
629 // If we are not currently active, we don't need to do anything. We will apply the format
630 // to the capture-session when we try to go active later.
631 //
632 // TODO: Determine if the incoming QCameraFormat resolves to the same formats
633 // that we are already using, in which case this function can be a no-op.
635 return true;
636
637 // We are active, so we need to reconfigure the entire capture-session with the
638 // new format.
640 if (!avCaptureDeviceLock) {
641 qWarning() << "QAVFCamera::tryApplyCameraFormat: Failed to lock AVCaptureDevice when "
642 "trying to apply new QCameraFormat.";
643 return false;
644 }
645
647 QScopeGuard endConfigGuard { [this]() {
649 } };
650
652
657 if (!configureResult) {
658 qWarning()
659 << "Error when trying to activate camera with new format: "
661
664
665 return false;
666 }
667
668 return true;
669}
670
672{
673#ifdef Q_OS_MACOS
674 return newFormat.resolution();
675#else
676 // Check, that we have matching dimesnions.
680 return resolution;
681
682 // Either portrait but actually sizes of landscape, or
683 // landscape with dimensions of portrait - not what
684 // sample delegate will report (it depends on videoOrientation set).
689
690 return resolution;
691#endif // Q_OS_MACOS
692}
693
698
705
728
729// Gets the current rotationfor this QAVFCamera.
730// Returns the result in degrees, 0 to 360.
731// Will always return a result that is divisible by 90.
733{
736 else
737 return 0;
738}
739
740// The still photo finishing will be invoked on a background thread not
741// controlled by us, on the QAvfCapturePhotoOutputDelegate object.
742// Without proper synchronization, we can therefore end up in a
743// situation where the callback is invoked after the QAVFCamera object
744// is destroyed. The current approach is to have a thread-safe call that
745// tells the QAvfCameraPhotoOutputDelegate to discard the results.
747{
752 // We must have an AVCaptureDeviceVideoInput hooked up to our AVCaptureSession
753 // in order for the AVCapturePhotoOutput to be populated with correct values.
755
756 using namespace Qt::Literals::StringLiterals;
757
758 // TODO: We can potentially match the current QCameraFormat here,
759 // which might help us save some bandwidth with i.e YUV420
762 qCWarning(qLcCamera) << "Attempted to take a still photo with an AVCapturePhotoOutput that "
763 "does not support output with 32BGRA format.";
764 return q23::unexpected{ u"Internal camera configuration error"_s };
765 }
766
770
771 // Set the settings for this capture.
772 //
773 // TODO: In the future we should try to respect the size set by QImageCapture here.
774 // For now, we use the same size as whatever the AVCaptureDevice is currently using.
777
780
783
784 // If we mistakenly use settings that are not supported, captureWithSettings will
785 // throw an exception.
786 @try {
789 }
790 @catch (NSException *exception) {
792 u"Attempted to start still photo capture with "
793 "capture-settings that are not supported by AVCapturePhotoOutput: '%1'"_s
796
797 return q23::unexpected{ u"Internal camera configuration error"_s };
798 }
799 @finally {}
800
804 this,
809 this,
811
813
814 return {};
815}
816
818{
822}
823
825{
829}
830
831} // namespace QFFmpeg
832
833QT_END_NAMESPACE
834
835#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
QT_NAMESPACE_ALIAS_OBJC_CLASS(QAVFCapturePhotoOutputDelegate)