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