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