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/qavfsamplebufferdelegate_p.h>
11
12#include <QtMultimedia/private/qavfcameradebug_p.h>
13#include <QtMultimedia/private/qavfcamerautility_p.h>
14#include <QtMultimedia/private/qavfhelpers_p.h>
15#include <QtMultimedia/private/qmultimediautils_p.h>
16#include <QtMultimedia/private/qplatformmediacapture_p.h>
17
18#define AVMediaType XAVMediaType
19extern "C" {
20#include <libavutil/hwcontext_videotoolbox.h>
21#include <libavutil/hwcontext.h>
22}
23#undef AVMediaType
24
25QT_BEGIN_NAMESPACE
26
27namespace QFFmpeg {
28
29namespace {
30
31[[nodiscard]] QAVFSampleBufferDelegateTransform surfaceTransform(
32 const QFFmpeg::AvfCameraRotationTracker *rotationTracker,
33 const AVCaptureConnection *connection)
34{
35 QAVFSampleBufferDelegateTransform transform = {};
36
37 int captureAngle = 0;
38
39 if (rotationTracker != nullptr) {
40 captureAngle = rotationTracker->rotationDegrees();
41
42 bool cameraIsFrontFacing =
43 rotationTracker->avCaptureDevice() != nullptr
44 && rotationTracker->avCaptureDevice().position == AVCaptureDevicePositionFront;
45 if (cameraIsFrontFacing)
46 transform.presentationTransform.mirroredHorizontallyAfterRotation = true;
47 }
48
49 // In some situations, AVFoundation can set the AVCaptureConnection.videoRotationAgngle
50 // implicity and start rotating the pixel buffer before handing it back
51 // to us. In this case we want to account for this during preview and capture.
52 //
53 // This code assumes that AVCaptureConnection.videoRotationAngle returns degrees
54 // that are divisible by 90. This has been the case during testing.
55 int connectionAngle = 0;
56 if (connection) {
57 if (@available(macOS 14.0, iOS 17.0, *))
58 connectionAngle = std::lround(connection.videoRotationAngle);
59
60 if (connection.videoMirrored)
61 transform.surfaceTransform.mirroredHorizontallyAfterRotation = true;
62 }
63
64 transform.surfaceTransform.rotation = qVideoRotationFromDegrees(captureAngle - connectionAngle);
65
66 return transform;
67}
68
69// This function may return a nullptr if no suitable format was found.
70// The format may not be supported by FFmpeg.
71[[nodiscard]] static AVCaptureDeviceFormat* findSuitableAvCaptureDeviceFormat(
72 AVCaptureDevice *avCaptureDevice,
73 const QCameraFormat &format)
74{
75 Q_ASSERT(avCaptureDevice != nullptr);
76 Q_ASSERT(!format.isNull());
77
78 // First we try to find a device format equivalent to QCameraFormat
79 // that is supported by FFmpeg.
80 AVCaptureDeviceFormat *newDeviceFormat = qt_convert_to_capture_device_format(
81 avCaptureDevice,
82 format,
83 &QFFmpeg::isCVFormatSupported);
84
85 // If we can't find a AVCaptureDeviceFormat supported by FFmpeg,
86 // fall back to one not supported by FFmpeg.
87 if (!newDeviceFormat)
88 newDeviceFormat = qt_convert_to_capture_device_format(avCaptureDevice, format);
89
90 return newDeviceFormat;
91}
92
93[[nodiscard]] static q23::expected<CvPixelFormat, QString> tryFindVideoDataOutputPixelFormat(
94 QVideoFrameFormat::PixelFormat cameraPixelFormat,
95 CvPixelFormat inputCvPixFormat,
96 AVCaptureVideoDataOutput *avCaptureVideoDataOutput)
97{
98 Q_ASSERT(cameraPixelFormat != QVideoFrameFormat::PixelFormat::Format_Invalid);
99 Q_ASSERT(inputCvPixFormat != CvPixelFormatInvalid);
100 Q_ASSERT(avCaptureVideoDataOutput != nullptr);
101
102 using namespace Qt::Literals::StringLiterals;
103
104 if (avCaptureVideoDataOutput.availableVideoCVPixelFormatTypes.count == 0)
105 return q23::unexpected{
106 u"AVCaptureVideoDataOutput.availableVideoCVPixelFormatTypes is empty"_s };
107
108 auto bestScore = MinAVScore;
109 NSNumber *bestFormat = nullptr;
110 for (NSNumber *cvPixFmtNumber in avCaptureVideoDataOutput.availableVideoCVPixelFormatTypes) {
111 const CvPixelFormat cvPixFmt = [cvPixFmtNumber unsignedIntValue];
112 const QVideoFrameFormat::PixelFormat pixFmt = QAVFHelpers::fromCVPixelFormat(cvPixFmt);
113 if (pixFmt == QVideoFrameFormat::Format_Invalid)
114 continue;
115
116 auto score = DefaultAVScore;
117 if (cvPixFmt == inputCvPixFormat)
118 score += 100;
119 if (pixFmt == cameraPixelFormat)
120 score += 10;
121 // if (cvPixFmt == kCVPixelFormatType_32BGRA)
122 // score += 1;
123
124 // This flag determines priorities of using ffmpeg hw frames or
125 // the exact camera format match.
126 // Maybe configure more, e.g. by some env var?
127 constexpr bool ShouldSuppressNotSupportedByFFmpeg = false;
128
129 if (!isCVFormatSupported(cvPixFmt))
130 score -= ShouldSuppressNotSupportedByFFmpeg ? 100000 : 5;
131
132 if (score > bestScore) {
133 bestScore = score;
134 bestFormat = cvPixFmtNumber;
135 }
136 }
137
138 if (bestScore < DefaultAVScore)
139 qWarning() << "QAVFCamera::tryFindVideoDataOutputPixelFormat: "
140 "Cannot find hw FFmpeg supported cv pix format";
141
142 return [bestFormat unsignedIntValue];
143}
144
145}
146
148{
149 return std::make_unique<QAVFCamera>(parent);
150}
151
159
161{
163
165 // Clearing the output will flush jobs on the dispatch queue running on a worker threadpool.
168
170}
171
173{
178 }
179}
180
183{
184 // AVCaptureDeviceInput.deviceInputWithDevice will implicitly ask for permission
185 // and present a dialogue to the end-user.
186 // Permission should only be requested explicitly through QPermission API.
188 Q_ASSERT(avCaptureDevice != nullptr);
189 Q_ASSERT(m_avCaptureSession != nullptr);
191
192 using namespace Qt::Literals::StringLiterals;
193
195
196 NSError* creationError = nullptr;
200 if (creationError != nullptr)
202
204 return q23::unexpected{
205 u"Cannot attach AVCaptureDeviceInput to AVCaptureSession"_s };
206
208
210
212
213 return {};
214}
215
216// If there is any current delegate, we block the background thread
217// and set the delegate to discard future frames.
219{
220 if (m_avCaptureVideoDataOutput != nullptr) {
224 }
225 if (m_qAvfSampleBufferDelegate != nullptr) {
226 // Push a blocking job to the background frame thread,
227 // so we guarantee future frames are discarded. This
228 // causes the frameHandler to be destroyed, and the reference
229 // to this QAVFCamera is cleared.
233 [this]() {
235 });
236
239 }
240}
241
244{
246
247 using namespace Qt::Literals::StringLiterals;
248
250
251 // Setup the delegate object for which we receive video frames.
252 // This is called by the background thread. The frameHandler must
253 // be cleared on the Delegate when destroying the QAVFCamera,
254 // to avoid any remaining enqueued frame-jobs from reading this QAVFCamera
255 // reference.
256 auto frameHandler = [this](QVideoFrame frame) {
259 };
260
264 // The transformProvider callable needs to be copyable, so we use a shared-ptr here.
268 return surfaceTransform(
270 connection);
271 }];
272
273 // Create the AVCaptureOutput object with our delegate object and background-thread.
275 init]
279
281 return q23::unexpected{
282 u"Unable to connect AVCaptureVideoDataOutput to AVCaptureSession"_s };
283
287
288 return {};
289}
290
291// This function writes to the AVCaptureVideoDataOutput and QAVFSampleBufferDelegate
292// objects directly. Don't use this function if these objects are already
293// connected to a running AVCaptureSession.
298{
299 Q_ASSERT(avCaptureDevice != nullptr);
302
305
306 // We cannot always use the AVCaptureDeviceFormat directly,
307 // so we look for a pixel format that we can use for the output.
308 // The AVFoundation internals will take care of converting the
309 // pixel formats to what we require.
317
319
320 // If the input AVCaptureDevice pixel format does not match
321 // the output pixel format, the AVFoundation internals will perform
322 // the conversion for us. This likely incurs performance overhead.
324 qCWarning(qLcCamera) << "Output CV format differs with capture device format!"
326 << "vs"
328 }
329
331
333
335 qCWarning(qLcCamera) << "Videotoolbox doesn't support cvPixelFormat:" << outputCvPixelFormat
337 << "Camera pix format:" << newCameraFormat.pixelFormat();
338 } else {
340 qCDebug(qLcCamera) << "Create VIDEOTOOLBOX hw context" << hwAccel.get() << "for camera";
341 }
342
343 // Apply the format to our capture session and QAVFCamera.
344
345 if (hwAccel) {
348 } else {
350 }
351
355
361 };
363
365
368
369 return {};
370}
371
373{
375}
376
378{
379 Q_ASSERT(avCaptureDevice != nullptr);
381}
382
384{
388}
389
393{
394 using namespace Qt::Literals::StringLiterals;
395
397 if (avCaptureDevice == nullptr)
398 return q23::unexpected{ u"AVCaptureDevice not available"_s };
399
403}
404
408{
409 using namespace Qt::Literals::StringLiterals;
410
414 // If we can't find any suitable AVCaptureDeviceFormat,
415 // then we cannot apply this QCameraFormat.
416 if (avCaptureDeviceFormat == nullptr)
417 return q23::unexpected{
418 u"Unable to find any suitable AVCaptureDeviceFormat when attempting to "
419 "apply QCameraFormat"_s };
420
425}
426
431{
432 Q_ASSERT(avCaptureDevice != nullptr);
434
437 if (!setupInputResult)
439
444
451
453
454 return {};
455}
456
458{
459 if (active) {
460 // We should never try to go active if we don't already have
461 // permissions, as refreshAvCaptureSessionInputDevice() will
462 // implicitly trigger a user permission request and freeze the
463 // program. Permissions should only be requested through
464 // QPermissions.
466
468 if (avCaptureDevice == nullptr) {
469 qWarning() << "QAVFCamera::onActiveChanged: Device not available";
470 return;
471 }
472
473 // The AVCaptureDevice must be locked when we call AVCaptureSession.startRunning,
474 // in order to not have the AVCaptureDeviceFormat be overriden by the AVCaptureSession's
475 // quality preset. Additionally, we apply the format inside tryConfigureCaptureSession,
476 // so it's beneficial to keep the device locked during the entire config stage.
478 if (!avCaptureDeviceLock) {
479 qWarning() << "QAVFCamera::onActiveChanged: Failed to lock AVCaptureDevice";
480 return;
481 }
482
485 cameraFormat());
486 if (configureResult) {
488 } else {
489 qWarning()
490 << "QAVFCamera::onActiveChanged: Error when trying to activate camera:"
493 }
494
495 } else {
497
499 }
500}
501
506
508{
509 // We cannot call AVCaptureSession.stopRunning() inside a
510 // AVCaptureSession configuration scope, so we wrap that scope in
511 // a lambda and call stopRunning() afterwards if configuration
512 // fails for the new QCameraDevice.
513
514 auto tryChangeDeviceFn = [this, &newCameraDevice]() -> q23::expected<void, QString> {
515 // Using this configuration transaction, we can clear up
516 // resources and establish new ones without having to do slow
517 // and synchronous calls to AVCaptureSession.stopRunning and startRunning.
521 } };
522
524
525 // If the new QCameraDevice does not point to any physical device,
526 // make sure we clear resources and shut down the capture-session.
528 return {};
529
530 // If we are not currently active, then we can just accept the new property
531 // value and return.
533 return {};
534
537 cameraFormat());
538 if (!configureResult) {
540 return configureResult;
541 }
542
543 return {};
544 };
545
547 if (!changeDeviceResult) {
549 qWarning()
550 << "Error when trying to activate new camera-device: "
552 }
553}
554
556{
559
560 // TODO: It's currently unclear whether we should accept the QCameraFormat
561 // if the QCameraDevice is currently not connected.
563 if (!avCaptureDevice)
564 return false;
565
569 // If we can't find any suitable AVCaptureDeviceFormat,
570 // then we cannot apply this QCameraFormat.
572 qWarning() << "QAVFCamera::tryApplyCameraFormat: Unable to find any suitable "
573 "AVCaptureDeviceFormat when attempting to apply QCameraFormat";
574 return false;
575 }
576
577 // If we are not currently active, we don't need to do anything. We will apply the format
578 // to the capture-session when we try to go active later.
579 //
580 // TODO: Determine if the incoming QCameraFormat resolves to the same formats
581 // that we are already using, in which case this function can be a no-op.
583 return true;
584
585 // We are active, so we need to reconfigure the entire capture-session with the
586 // new format.
588 if (!avCaptureDeviceLock) {
589 qWarning() << "QAVFCamera::tryApplyCameraFormat: Failed to lock AVCaptureDevice when "
590 "trying to apply new QCameraFormat.";
591 return false;
592 }
593
595 QScopeGuard endConfigGuard { [this]() {
597 } };
598
600
605 if (!configureResult) {
606 qWarning()
607 << "Error when trying to activate camera with new format: "
609
612
613 return false;
614 }
615
616 return true;
617}
618
620{
621#ifdef Q_OS_MACOS
622 return newFormat.resolution();
623#else
624 // Check, that we have matching dimesnions.
628 return resolution;
629
630 // Either portrait but actually sizes of landscape, or
631 // landscape with dimensions of portrait - not what
632 // sample delegate will report (it depends on videoOrientation set).
637
638 return resolution;
639#endif // Q_OS_MACOS
640}
641
646
653
676
677// Gets the current rotationfor this QAVFCamera.
678// Returns the result in degrees, 0 to 360.
679// Will always return a result that is divisible by 90.
681{
684 else
685 return 0;
686}
687
688} // namespace QFFmpeg
689
690QT_END_NAMESPACE
691
692#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