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