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
qavfvideodevices.mm
Go to the documentation of this file.
1// Copyright (C) 2025 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 <QtMultimedia/private/qavfvideodevices_p.h>
5#include <QtMultimedia/private/qcameradevice_p.h>
6#include <QtMultimedia/private/qavfhelpers_p.h>
7#include <QtMultimedia/private/qavfcamerautility_p.h>
8
9#include <QtCore/qcoreapplication.h>
10#include <QtCore/qloggingcategory.h>
11#include <QtCore/private/qexpected_p.h>
12#include <QtCore/qset.h>
13#include <QtCore/qspan.h>
14#include <QtCore/qthread.h>
15
16#include <vector>
17
19
20Q_STATIC_LOGGING_CATEGORY(qLcAvfVideoDevices, "qt.multimedia.avfvideodevices");
21
22using namespace Qt::Literals::StringLiterals;
23
24namespace {
25
26// Helper function to translate AVCaptureDevicePosition enum to QCameraDevice::Position enum.
27[[nodiscard]] QCameraDevice::Position qAvfToQCameraDevicePosition(AVCaptureDevicePosition input)
28{
29 switch (input) {
30 case AVCaptureDevicePositionFront:
31 return QCameraDevice::Position::FrontFace;
32 case AVCaptureDevicePositionBack:
33 return QCameraDevice::Position::BackFace;
34 default:
35 return QCameraDevice::Position::UnspecifiedPosition;
36 }
37}
38
39// Error message is not user-facing.
40[[nodiscard]] q23::expected<AVFScopedPointer<AVCaptureDeviceDiscoverySession>, QString>
41createAvCaptureDeviceDiscoverySession()
42{
43 // List of all capture device types that we want to discover. Seems that this is the
44 // only way to discover all types. This filter is mandatory and has no "unspecified"
45 // option like AVCaptureDevicePosition(Unspecified) has. Order of the list is important
46 // because discovered devices will be in the same order and we want the first one found
47 // to be our default device.
48 NSArray *discoveryDevices = @[
49#ifdef Q_OS_IOS
50 AVCaptureDeviceTypeBuiltInTripleCamera, // We always prefer triple camera.
51 AVCaptureDeviceTypeBuiltInDualCamera, // If triple is not available, we prefer
52 // dual with wide + tele lens.
53 AVCaptureDeviceTypeBuiltInDualWideCamera, // Dual with wide and ultrawide is still
54 // better than single.
55#endif
56 AVCaptureDeviceTypeBuiltInWideAngleCamera, // This is the most common single camera type.
57 // We prefer that over tele and ultra-wide.
58#ifdef Q_OS_IOS
59 AVCaptureDeviceTypeBuiltInTelephotoCamera, // Cannot imagine how, but if only tele and
60 // ultrawide are available, we prefer tele.
61 AVCaptureDeviceTypeBuiltInUltraWideCamera,
62#endif
63 ];
64
65 if (@available(macOS 14, *)) {
66 discoveryDevices = [discoveryDevices arrayByAddingObjectsFromArray: @[
67 AVCaptureDeviceTypeExternal,
68 AVCaptureDeviceTypeContinuityCamera
69 ]];
70 } else {
71#ifdef Q_OS_MACOS
72 QT_WARNING_PUSH
73 QT_WARNING_DISABLE_DEPRECATED
74 discoveryDevices = [discoveryDevices arrayByAddingObjectsFromArray: @[
75 AVCaptureDeviceTypeExternalUnknown
76 ]];
77 QT_WARNING_POP
78#endif
79 }
80
81 @try {
82 // Create discovery session to discover all possible camera types of the system.
83 // Both "hard" and "soft" types.
84 AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
85 discoverySessionWithDeviceTypes:discoveryDevices
86 mediaType:AVMediaTypeVideo
87 position:AVCaptureDevicePositionUnspecified];
88 return AVFScopedPointer{ [discoverySession retain] };
89 }
90 @catch (NSException *e) {
91 return q23::unexpected{
92 u"Exception caught when trying to create AVCaptureDeviceDiscoverySession: "_s
93 + QString::fromNSString(e.reason) };
94 }
95}
96
97// Given a list of AVCaptureDevices, returns a list of all the QCameraDevices
98// we want to expose to the user.
99// Thread-safe
100template <typename FormatChecker>
101[[nodiscard]] QList<QCameraDevice>
102qGenerateQCameraDevices(NSArray<AVCaptureDevice *> *videoDevices,
103 const FormatChecker &isCvPixelFormatSupported)
104{
105 QList<QCameraDevice> cameras;
106
107 for (AVCaptureDevice *device in videoDevices) {
108 if ([device isSuspended])
109 continue;
110
111 auto info = std::make_unique<QCameraDevicePrivate>();
112 if ([videoDevices[0].uniqueID isEqualToString:device.uniqueID])
113 info->isDefault = true;
114 info->id = QByteArray([[device uniqueID] UTF8String]);
115 info->description = QString::fromNSString([device localizedName]);
116 info->position = qAvfToQCameraDevicePosition([device position]);
117
118 qCDebug(qLcAvfVideoDevices) << "Handling camera info" << info->description
119 << (info->isDefault ? "(default)" : "");
120
121 QSet<QSize> photoResolutions;
122 QList<QCameraFormat> videoFormats;
123
124 for (AVCaptureDeviceFormat *format in device.formats) {
125 if (![format.mediaType isEqualToString:AVMediaTypeVideo])
126 continue;
127
128 const CMVideoDimensions dimensions =
129 CMVideoFormatDescriptionGetDimensions(format.formatDescription);
130 QSize resolution(dimensions.width, dimensions.height);
131 photoResolutions.insert(resolution);
132
133 float maxFrameRate = 0;
134 float minFrameRate = 1.e6;
135
136 const CvPixelFormat cvPixelFormat =
137 CMVideoFormatDescriptionGetCodecType(format.formatDescription);
138
139 // Don't expose formats if the media backend says we can't start a capture session
140 // with it.
141 if (!isCvPixelFormatSupported(cvPixelFormat))
142 continue;
143
144 const QVideoFrameFormat::PixelFormat pixelFormat =
145 QAVFHelpers::fromCVPixelFormat(cvPixelFormat);
146 const QVideoFrameFormat::ColorRange colorRange =
147 QAVFHelpers::colorRangeForCVPixelFormat(cvPixelFormat);
148
149 // Ignore pixel formats we can't handle
150 if (pixelFormat == QVideoFrameFormat::Format_Invalid) {
151 qCDebug(qLcAvfVideoDevices) << "ignore camera CV format" << cvPixelFormat
152 << "as no matching video format found";
153 continue;
154 }
155
156 for (const AVFrameRateRange *frameRateRange in format.videoSupportedFrameRateRanges) {
157 if (frameRateRange.minFrameRate < minFrameRate)
158 minFrameRate = frameRateRange.minFrameRate;
159 if (frameRateRange.maxFrameRate > maxFrameRate)
160 maxFrameRate = frameRateRange.maxFrameRate;
161 }
162
163#ifdef Q_OS_IOS
164 // From Apple's docs (iOS):
165 // By default, AVCaptureStillImageOutput emits images with the same dimensions as
166 // its source AVCaptureDevice instance’s activeFormat.formatDescription. However,
167 // if you set this property to YES, the receiver emits still images at the capture
168 // device’s highResolutionStillImageDimensions value.
169 const QSize hrRes(qt_device_format_high_resolution(format));
170 if (!hrRes.isNull() && hrRes.isValid())
171 photoResolutions.insert(hrRes);
172#endif
173
174 qCDebug(qLcAvfVideoDevices) << "Add camera format. pixelFormat:" << pixelFormat
175 << "colorRange:" << colorRange << "cvPixelFormat" << cvPixelFormat
176 << "resolution:" << resolution << "frameRate: [" << minFrameRate
177 << maxFrameRate << "]";
178
179 auto *f = new QCameraFormatPrivate{ QSharedData(), pixelFormat, resolution,
180 minFrameRate, maxFrameRate, colorRange };
181 videoFormats << f->create();
182 }
183 if (videoFormats.isEmpty()) {
184 // skip broken cameras without valid formats
185 qCWarning(qLcAvfVideoDevices())
186 << "Skip camera" << info->description << "without supported formats";
187 continue;
188 }
189 info->videoFormats = videoFormats;
190 info->photoResolutions = photoResolutions.values();
191
192 cameras.append(info.release()->create());
193 }
194
195 return cameras;
196}
197
198} // Unnamed namespace
199
200// Can be called by any thread
201QAVFVideoDevices::QAVFVideoDevices(
202 QPlatformMediaIntegration *integration,
203 std::function<bool(uint32_t)> &&isCvPixelFormatSupportedDelegate)
204 : QPlatformVideoDevices(integration),
205 m_isCvPixelFormatSupportedDelegate(std::move(isCvPixelFormatSupportedDelegate))
206{
207 Q_ASSERT(QCoreApplication::instance());
208 moveToThread(QCoreApplication::instance()->thread());
209
210 // Calling thread might not have any dispatch_queue or autorelease pool.
211 QMacAutoReleasePool autoReleasePool;
212
213 auto discoverySessionResult = createAvCaptureDeviceDiscoverySession();
214 if (!discoverySessionResult) {
215 qCWarning(qLcAvfVideoDevices) << discoverySessionResult.error();
216 qWarning() << "Failed to establish camera device discovery session. "
217 "QMediaDevices::videoInputs() will not work.";
218 return;
219 }
220
221 m_avDiscoverySession = std::move(*discoverySessionResult);
222 m_avDiscoverySessionObserver = QMacKeyValueObserver(
223 m_avDiscoverySession,
224 @"devices",
225 [this] {
226 onAvCaptureDevicesChanged();
227 });
228
229 // Setup initial list of observed AVCaptureDevices.
230 QMetaObject::invokeMethod(this, [this]{
231 rebuildObserveredAvCaptureDevices();
232 });
233}
234
235QAVFVideoDevices::~QAVFVideoDevices() = default;
236
237// Can be called from any thread as result of QMediaDevices::videoInputs()
238QList<QCameraDevice> QAVFVideoDevices::findVideoInputs() const
239{
240 if (!m_avDiscoverySession)
241 return {};
242
243 // This function can be called from any thread, including
244 // threads with no dispatch_queue, so we need an autorelease pool.
245 QMacAutoReleasePool autoReleasePool;
246
247 NSArray<AVCaptureDevice *> *deviceList = m_avDiscoverySession.data().devices;
248 Q_ASSERT(deviceList);
249
250 return qGenerateQCameraDevices(deviceList, [this](uint32_t cvPixelFormat) {
251 return isCvPixelFormatSupported(cvPixelFormat);
252 });
253}
254
255bool QAVFVideoDevices::isCvPixelFormatSupported(uint32_t cvPixelFormat) const
256{
257 return !m_isCvPixelFormatSupportedDelegate || m_isCvPixelFormatSupportedDelegate(cvPixelFormat);
258}
259
260// Refreshes list of connected AVCaptureDevices and their key-value observers.
261void QAVFVideoDevices::rebuildObserveredAvCaptureDevices()
262{
263 Q_ASSERT(QCoreApplication::instance()->thread()->isCurrentThread());
264
265 m_observedAvCaptureDevices.clear();
266
267 if (!m_avDiscoverySession)
268 return;
269
270 NSArray<AVCaptureDevice *> *deviceList = m_avDiscoverySession.data().devices;
271 Q_ASSERT(deviceList);
272
273 m_observedAvCaptureDevices.reserve(deviceList.count);
274
275 for (AVCaptureDevice *captureDevice in deviceList) {
276 AVFScopedPointer retainedDevice{ [captureDevice retain] };
277
278 // When the suspended value changes, post an update job to QAVFVideoDevices.
279 QMacKeyValueObserver observer(
280 captureDevice,
281 @"suspended",
282 [this] {
283 onAvCaptureDevicesChanged();
284 });
285
286 m_observedAvCaptureDevices.push_back({ std::move(retainedDevice), std::move(observer) });
287 }
288}
289
290void QAVFVideoDevices::onAvCaptureDevicesChanged()
291{
292 // Callbacks can potentially get invoked in cocoa threads.
293 // Post a job to the object's thread.
294 QMetaObject::invokeMethod(this, [this] {
295 rebuildObserveredAvCaptureDevices();
296 onVideoInputsChanged();
297 });
298}
299
300QT_END_NAMESPACE
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)