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 using namespace Qt::Literals::StringLiterals;
498
499 if (active) {
500 // We should never try to go active if we don't already have
501 // permissions, as refreshAvCaptureSessionInputDevice() will
502 // implicitly trigger a user permission request and freeze the
503 // program. Permissions should only be requested through
504 // QPermissions.
506
508 if (avCaptureDevice == nullptr) {
509 qCWarning(qLcCamera) << "No AVCaptureDevice found for camera device"
510 << m_cameraDevice.id();
511 return q23::unexpected{ u"Camera device is not available"_s };
512 }
513
514 // The AVCaptureDevice must be locked when we call AVCaptureSession.startRunning,
515 // in order to not have the AVCaptureDeviceFormat be overriden by the AVCaptureSession's
516 // quality preset. Additionally, we apply the format inside tryConfigureCaptureSession,
517 // so it's beneficial to keep the device locked during the entire config stage.
519 if (!avCaptureDeviceLock) {
520 qCWarning(qLcCamera) << "Failed to lock AVCaptureDevice for configuration";
521 return q23::unexpected{ u"Failed to start camera"_s };
522 }
523
526 cameraFormat());
527 if (!configureResult) {
528 qCWarning(qLcCamera) << "Failed to configure AVCaptureSession:"
531 return q23::unexpected{ u"Failed to start camera"_s };
532 }
533
535 } else {
537
539 }
540
541 return {};
542}
543
548
552{
553 // The incoming format should never be null if the incoming device is not null.
555
556 // We cannot call AVCaptureSession.stopRunning() inside a
557 // AVCaptureSession configuration scope, so we wrap that scope in
558 // a lambda and call stopRunning() afterwards if configuration
559 // fails for the new QCameraDevice.
560
561 auto tryChangeDeviceFn = [&]() -> q23::expected<void, QString> {
562 // Using this configuration transaction, we can clear up
563 // resources and establish new ones without having to do slow
564 // and synchronous calls to AVCaptureSession.stopRunning and startRunning.
568 } };
569
571
572 // If the new QCameraDevice does not point to any physical device,
573 // make sure we clear resources and shut down the capture-session.
575 return {};
576
577 // If we are not currently active, then we can just accept the new property
578 // value and return.
580 return {};
581
584 newFormat);
585 if (!configureResult) {
587 return configureResult;
588 }
589
590 return {};
591 };
592
594 if (!changeDeviceResult) {
596 qWarning()
597 << "Error when trying to activate new camera-device: "
599 }
600}
601
603{
606
607 // TODO: It's currently unclear whether we should accept the QCameraFormat
608 // if the QCameraDevice is currently not connected.
610 if (!avCaptureDevice)
611 return false;
612
616 // If we can't find any suitable AVCaptureDeviceFormat,
617 // then we cannot apply this QCameraFormat.
619 qWarning() << "QAVFCamera::tryApplyCameraFormat: Unable to find any suitable "
620 "AVCaptureDeviceFormat when attempting to apply QCameraFormat";
621 return false;
622 }
623
624 // If we are not currently active, we don't need to do anything. We will apply the format
625 // to the capture-session when we try to go active later.
626 //
627 // TODO: Determine if the incoming QCameraFormat resolves to the same formats
628 // that we are already using, in which case this function can be a no-op.
630 return true;
631
632 // We are active, so we need to reconfigure the entire capture-session with the
633 // new format.
635 if (!avCaptureDeviceLock) {
636 qWarning() << "QAVFCamera::tryApplyCameraFormat: Failed to lock AVCaptureDevice when "
637 "trying to apply new QCameraFormat.";
638 return false;
639 }
640
642 QScopeGuard endConfigGuard { [this]() {
644 } };
645
647
652 if (!configureResult) {
653 qWarning()
654 << "Error when trying to activate camera with new format: "
656
659
660 return false;
661 }
662
663 return true;
664}
665
667{
668#ifdef Q_OS_MACOS
669 return newFormat.resolution();
670#else
671 // Check, that we have matching dimesnions.
675 return resolution;
676
677 // Either portrait but actually sizes of landscape, or
678 // landscape with dimensions of portrait - not what
679 // sample delegate will report (it depends on videoOrientation set).
684
685 return resolution;
686#endif // Q_OS_MACOS
687}
688
693
700
723
724// Gets the current rotationfor this QAVFCamera.
725// Returns the result in degrees, 0 to 360.
726// Will always return a result that is divisible by 90.
728{
731 else
732 return 0;
733}
734
735// The still photo finishing will be invoked on a background thread not
736// controlled by us, on the QAvfCapturePhotoOutputDelegate object.
737// Without proper synchronization, we can therefore end up in a
738// situation where the callback is invoked after the QAVFCamera object
739// is destroyed. The current approach is to have a thread-safe call that
740// tells the QAvfCameraPhotoOutputDelegate to discard the results.
742{
747 // We must have an AVCaptureDeviceVideoInput hooked up to our AVCaptureSession
748 // in order for the AVCapturePhotoOutput to be populated with correct values.
750
751 using namespace Qt::Literals::StringLiterals;
752
753 // TODO: We can potentially match the current QCameraFormat here,
754 // which might help us save some bandwidth with i.e YUV420
757 qCWarning(qLcCamera) << "Attempted to take a still photo with an AVCapturePhotoOutput that "
758 "does not support output with 32BGRA format.";
759 return q23::unexpected{ u"Internal camera configuration error"_s };
760 }
761
763
764 // Set the settings for this capture.
765 //
766 // TODO: In the future we should try to respect the size set by QImageCapture here.
767 // For now, we use the same size as whatever the AVCaptureDevice is currently using.
770
773
776
777 // If we mistakenly use settings that are not supported, captureWithSettings will
778 // throw an exception.
779 @try {
782 }
783 @catch (NSException *exception) {
785 u"Attempted to start still photo capture with "
786 "capture-settings that are not supported by AVCapturePhotoOutput: '%1'"_s
789
790 return q23::unexpected{ u"Internal camera configuration error"_s };
791 }
792 @finally {}
793
797 this,
802 this,
804
806
807 return {};
808}
809
811{
815}
816
818{
822}
823
824} // namespace QFFmpeg
825
826QT_END_NAMESPACE
827
828#include "moc_qavfcamera_p.cpp"
std::unique_ptr< QPlatformCamera > makeQAvfCamera(QCamera &parent)