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
399 };
401
403
406
407 return {};
408}
409
411{
413}
414
416{
417 Q_ASSERT(avCaptureDevice != nullptr);
419}
420
422{
426}
427
431{
432 using namespace Qt::Literals::StringLiterals;
433
435 if (avCaptureDevice == nullptr)
436 return q23::unexpected{ u"AVCaptureDevice not available"_s };
437
441}
442
446{
447 using namespace Qt::Literals::StringLiterals;
448
452 // If we can't find any suitable AVCaptureDeviceFormat,
453 // then we cannot apply this QCameraFormat.
454 if (avCaptureDeviceFormat == nullptr)
455 return q23::unexpected{
456 u"Unable to find any suitable AVCaptureDeviceFormat when attempting to "
457 "apply QCameraFormat"_s };
458
463}
464
469{
472
475 if (!setupInputResult)
477
482
489
491
492 return {};
493}
494
496{
497 if (active) {
498 // We should never try to go active if we don't already have
499 // permissions, as refreshAvCaptureSessionInputDevice() will
500 // implicitly trigger a user permission request and freeze the
501 // program. Permissions should only be requested through
502 // QPermissions.
504
506 if (avCaptureDevice == nullptr) {
507 qWarning() << "QAVFCamera::onActiveChanged: Device not available";
508 return;
509 }
510
511 // The AVCaptureDevice must be locked when we call AVCaptureSession.startRunning,
512 // in order to not have the AVCaptureDeviceFormat be overriden by the AVCaptureSession's
513 // quality preset. Additionally, we apply the format inside tryConfigureCaptureSession,
514 // so it's beneficial to keep the device locked during the entire config stage.
516 if (!avCaptureDeviceLock) {
517 qWarning() << "QAVFCamera::onActiveChanged: Failed to lock AVCaptureDevice";
518 return;
519 }
520
523 cameraFormat());
524 if (configureResult) {
526 } else {
527 qWarning()
528 << "QAVFCamera::onActiveChanged: Error when trying to activate camera:"
531 }
532
533 } else {
535
537 }
538}
539
544
548{
549 // The incoming format should never be null if the incoming device is not null.
551
552 // We cannot call AVCaptureSession.stopRunning() inside a
553 // AVCaptureSession configuration scope, so we wrap that scope in
554 // a lambda and call stopRunning() afterwards if configuration
555 // fails for the new QCameraDevice.
556
557 auto tryChangeDeviceFn = [&]() -> q23::expected<void, QString> {
558 // Using this configuration transaction, we can clear up
559 // resources and establish new ones without having to do slow
560 // and synchronous calls to AVCaptureSession.stopRunning and startRunning.
564 } };
565
567
568 // If the new QCameraDevice does not point to any physical device,
569 // make sure we clear resources and shut down the capture-session.
571 return {};
572
573 // If we are not currently active, then we can just accept the new property
574 // value and return.
576 return {};
577
580 newFormat);
581 if (!configureResult) {
583 return configureResult;
584 }
585
586 return {};
587 };
588
590 if (!changeDeviceResult) {
592 qWarning()
593 << "Error when trying to activate new camera-device: "
595 }
596}
597
599{
602
603 // TODO: It's currently unclear whether we should accept the QCameraFormat
604 // if the QCameraDevice is currently not connected.
606 if (!avCaptureDevice)
607 return false;
608
612 // If we can't find any suitable AVCaptureDeviceFormat,
613 // then we cannot apply this QCameraFormat.
615 qWarning() << "QAVFCamera::tryApplyCameraFormat: Unable to find any suitable "
616 "AVCaptureDeviceFormat when attempting to apply QCameraFormat";
617 return false;
618 }
619
620 // If we are not currently active, we don't need to do anything. We will apply the format
621 // to the capture-session when we try to go active later.
622 //
623 // TODO: Determine if the incoming QCameraFormat resolves to the same formats
624 // that we are already using, in which case this function can be a no-op.
626 return true;
627
628 // We are active, so we need to reconfigure the entire capture-session with the
629 // new format.
631 if (!avCaptureDeviceLock) {
632 qWarning() << "QAVFCamera::tryApplyCameraFormat: Failed to lock AVCaptureDevice when "
633 "trying to apply new QCameraFormat.";
634 return false;
635 }
636
638 QScopeGuard endConfigGuard { [this]() {
640 } };
641
643
648 if (!configureResult) {
649 qWarning()
650 << "Error when trying to activate camera with new format: "
652
655
656 return false;
657 }
658
659 return true;
660}
661
663{
664#ifdef Q_OS_MACOS
665 return newFormat.resolution();
666#else
667 // Check, that we have matching dimesnions.
671 return resolution;
672
673 // Either portrait but actually sizes of landscape, or
674 // landscape with dimensions of portrait - not what
675 // sample delegate will report (it depends on videoOrientation set).
680
681 return resolution;
682#endif // Q_OS_MACOS
683}
684
689
696
719
720// Gets the current rotationfor this QAVFCamera.
721// Returns the result in degrees, 0 to 360.
722// Will always return a result that is divisible by 90.
724{
727 else
728 return 0;
729}
730
731// The still photo finishing will be invoked on a background thread not
732// controlled by us, on the QAvfCapturePhotoOutputDelegate object.
733// Without proper synchronization, we can therefore end up in a
734// situation where the callback is invoked after the QAVFCamera object
735// is destroyed. The current approach is to have a thread-safe call that
736// tells the QAvfCameraPhotoOutputDelegate to discard the results.
738{
743 // We must have an AVCaptureDeviceVideoInput hooked up to our AVCaptureSession
744 // in order for the AVCapturePhotoOutput to be populated with correct values.
746
747 using namespace Qt::Literals::StringLiterals;
748
749 // TODO: We can potentially match the current QCameraFormat here,
750 // which might help us save some bandwidth with i.e YUV420
753 qCWarning(qLcCamera) << "Attempted to take a still photo with an AVCapturePhotoOutput that "
754 "does not support output with 32BGRA format.";
755 return q23::unexpected{ u"Internal camera configuration error"_s };
756 }
757
759
760 // Set the settings for this capture.
761 //
762 // TODO: In the future we should try to respect the size set by QImageCapture here.
763 // For now, we use the same size as whatever the AVCaptureDevice is currently using.
766
769
772
773 // If we mistakenly use settings that are not supported, captureWithSettings will
774 // throw an exception.
775 @try {
778 }
779 @catch (NSException *exception) {
781 u"Attempted to start still photo capture with "
782 "capture-settings that are not supported by AVCapturePhotoOutput: '%1'"_s
785
786 return q23::unexpected{ u"Internal camera configuration error"_s };
787 }
788 @finally {}
789
793 this,
798 this,
800
802
803 return {};
804}
805
807{
811}
812
814{
818}
819
820} // namespace QFFmpeg
821
822QT_END_NAMESPACE
823
824#include "moc_qavfcamera_p.cpp"
std::unique_ptr< QPlatformCamera > makeQAvfCamera(QCamera &parent)
QT_MANGLE_NAMESPACE(QMacScreenCaptureStreamDelegate) QMacScreenCaptureStreamDelegate