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
qsckwindowcapture.mm
Go to the documentation of this file.
1// Copyright (C) 2026 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
5
6#include <QtCore/qthread.h>
7#include <QtCore/private/qcore_mac_p.h>
8
9#include <QtFFmpegMediaPluginImpl/private/qffmpegmediacapturesession_p.h>
10#include <QtFFmpegMediaPluginImpl/private/qmacscreencapturekit_p.h>
11
12#include <QtMultimedia/private/qmultimedia_ranges_p.h>
13#include <QtMultimedia/private/qcapturablewindow_p.h>
14#include <QtMultimedia/private/qvideoframe_p.h>
15
16#define AVMediaType XAVMediaType
17extern "C" {
18#include <libavutil/hwcontext_videotoolbox.h>
19}
20#undef AVMediaType
21
22namespace ranges = QtMultimediaPrivate::ranges;
23using namespace Qt::Literals::StringLiterals;
24
25QT_BEGIN_NAMESPACE
26
27namespace QFFmpeg {
28
29namespace {
30
31[[nodiscard]] q23::expected<AVFScopedPointer<SCWindow>, QString> findScWindow(CGWindowID input)
32{
33 // Do a blocking enumeration of capturable items.
34 q23::expected<QMacScreenCaptureKit::CapturableItems, QString> enumerateResult =
35 QMacScreenCaptureKit::enumerateCapturableItems()
36 .get();
37
38 if (!enumerateResult)
39 return q23::unexpected(enumerateResult.error());
40
41 const std::vector<AVFScopedPointer<SCWindow>> &windows = enumerateResult->windows;
42 auto it = ranges::find_if(
43 windows,
44 [&](const AVFScopedPointer<SCWindow> &item) {
45 return item.data().windowID == input;
46 });
47
48 if (it == windows.end())
49 return q23::unexpected(u"Window not found"_s);
50
51 // AVFScopedPointer doesn't have shared-ptr semantics, force a reference increment.
52 return AVFScopedPointer<SCWindow>{ [it->data() retain] };
53}
54
55// Finds the SCWindow to capture for the given QCapturableWindow.
56[[nodiscard]] q23::expected<AVFScopedPointer<SCWindow>, QPlatformSurfaceCapture::ErrorDetails>
57findScWindowForSource(const QCapturableWindow &capturableWindow)
58{
59 using Error = QPlatformSurfaceCapture::Error;
60 using ErrorDetails = QPlatformSurfaceCapture::ErrorDetails;
61
62 const QCapturableWindowPrivate *handle = QCapturableWindowPrivate::handle(capturableWindow);
63 if (!handle)
64 return q23::unexpected{ ErrorDetails{
65 Error::NotFound,
66 u"No window has been selected for capture."_s } };
67 const auto cgWindowId = static_cast<CGWindowID>(handle->id);
68
69 // This will trigger the system dialog for granting screen capture permissions.
70 q23::expected<AVFScopedPointer<SCWindow>, QString> scWindowResult = findScWindow(cgWindowId);
71 if (!scWindowResult) {
72 qCWarning(qLcMacScreenCapture)
73 << "Could not find associated SCWindow:"
74 << scWindowResult.error();
75 return q23::unexpected{ ErrorDetails{
76 Error::NotFound,
77 u"The selected window could not be found."_s } };
78 }
79
80 return std::move(*scWindowResult);
81}
82
83void setupQMacScreenCaptureKitConnections(
84 QSckWindowCapture &windowCapture,
85 const QMacScreenCaptureKit &macScreenCaptureKit)
86{
87 // Direct connection so application developers can respond to frame directly
88 // from background thread.
89 // Remaining frames are always flushed whenever we go inactive, as a result
90 // of QMacScreenCaptureKit doing so in the destructor. Because we flush,
91 // we trust the application developer to not block the background thread.
92 QObject::connect(
93 &macScreenCaptureKit,
94 &QMacScreenCaptureKit::newVideoFrameGenerated,
95 &windowCapture,
96 [&windowCapture](QMacScreenCaptureKit::StreamId, QVideoFrame videoFrame) {
97 Q_EMIT windowCapture.newVideoFrame(videoFrame);
98 },
99 Qt::DirectConnection);
100
101 QObject::connect(
102 &macScreenCaptureKit,
103 &QMacScreenCaptureKit::newVideoFrameGenerated,
104 &windowCapture,
105 [&windowCapture](QMacScreenCaptureKit::StreamId streamId, QVideoFrame newFrame) {
106 windowCapture.onNewFrameFormatReceived(streamId, newFrame.surfaceFormat());
107 },
108 Qt::QueuedConnection);
109
110 QObject::connect(
111 &macScreenCaptureKit,
112 &QMacScreenCaptureKit::streamStoppedWithError,
113 &windowCapture,
114 &QSckWindowCapture::onStreamStoppedWithErrorEvent,
115 Qt::QueuedConnection);
116}
117
118} // Anonymous namespace
119
120QSckWindowCapture::QSckWindowCapture() : QPlatformSurfaceCapture(WindowSource{})
121{
122}
123
124void QSckWindowCapture::setCaptureSession(QPlatformMediaCaptureSession *sessionIn)
125{
126 if (!sessionIn) {
127 m_session = nullptr;
128 return;
129 }
130
131 auto session = qobject_cast<QFFmpegMediaCaptureSession *>(sessionIn);
132 Q_ASSERT(session);
133 m_session = session;
134}
135
137{
138 using TryStartResult = q23::expected<std::unique_ptr<ActiveData>, ErrorDetails>;
139
140 auto tryStartStream = [&]() -> TryStartResult {
141 // Find the associated SCWindow we can use.
142 q23::expected<AVFScopedPointer<SCWindow>, ErrorDetails> scWindowResult =
143 findScWindowForSource(source<WindowSource>());
144 if (!scWindowResult)
145 return q23::unexpected{ std::move(scWindowResult.error()) };
146
147 QMacScreenCaptureKit::StreamId newStreamId = allocateStreamId();
148
149 AVFScopedPointer<SCWindow> &scWindow = *scWindowResult;
150
151 // Tell capturesession to setup initial connections ahead
152 // of time as to not drop initial frames.
153 if (m_session)
154 m_session->onSourceActivating(*this);
155
156 QMacScreenCaptureKit::CreateStreamInfo createInfo = {};
157 createInfo.streamId = newStreamId;
158 createInfo.connectionSetupFn = [&](QMacScreenCaptureKit &newObject) {
159 setupQMacScreenCaptureKitConnections(*this, newObject);
160 };
161 createInfo.streamSettings.frameRate = frameRate();
162 createInfo.streamSettings.overrideIgnoreCursor = ignoreCursor();
163
164 // Start and wait for the stream to start. Blocking operation.
165 using ResultType = q23::expected<std::unique_ptr<QMacScreenCaptureKit>, QString>;
166 std::future<ResultType> streamResultFuture = QMacScreenCaptureKit::createStreamFromWindow(
167 createInfo,
168 scWindow.data());
169
170 ResultType streamResult = streamResultFuture.get();
171 if (!streamResult) {
172 // Tell capturesession to restore previous state.
173 if (m_session)
174 m_session->onSourceActivationFailure(*this);
175
176 qCWarning(qLcMacScreenCapture)
177 << "Failed to start screen capture stream:"
178 << streamResult.error();
179 return q23::unexpected{ ErrorDetails{
180 QPlatformSurfaceCapture::Error::CaptureFailed,
181 u"Window capture could not be started."_s } };
182 }
183
184 std::unique_ptr<QMacScreenCaptureKit> &macScreenCaptureKit = *streamResult;
185
186 auto newActiveData = std::make_unique<ActiveData>();
187 newActiveData->macScreenCaptureKit = std::move(macScreenCaptureKit);
188 newActiveData->scWindow = std::move(scWindow);
189 newActiveData->streamId = newStreamId;
190 return newActiveData;
191 };
192
193 if (active) {
194 Q_ASSERT(!m_activeData);
195
196 TryStartResult result = tryStartStream();
197 if (!result) {
198 const ErrorDetails &error = result.error();
199 QPlatformSurfaceCapture::updateError(error.error, error.description);
200 return false;
201 }
202
203 m_activeData = std::move(*result);
204 } else {
205 m_activeData.reset();
206 }
207
208 return true;
209}
210
212QSckWindowCapture::switchActiveSource(const Source &newSource)
213{
214 Q_ASSERT(m_activeData);
215
216 q23::expected<AVFScopedPointer<SCWindow>, ErrorDetails> scWindowResult =
217 findScWindowForSource(std::get<WindowSource>(newSource));
218 if (!scWindowResult)
219 return SourceSwitchResult{ q23::unexpected{ std::move(scWindowResult.error()) } };
220
221 std::future<q23::expected<void, QString>> switchFuture =
222 m_activeData->macScreenCaptureKit->switchToWindow(scWindowResult->data());
223
224 const q23::expected<void, QString> result = switchFuture.get();
225 if (!result) {
226 qCWarning(qLcMacScreenCapture)
227 << "Failed to switch screen capture stream to new window:"
228 << result.error();
229 return SourceSwitchResult{ q23::unexpected{ ErrorDetails{
230 Error::CaptureFailed,
231 u"Could not switch capture to the selected window."_s } } };
232 }
233
234 m_activeData->scWindow = std::move(*scWindowResult);
235 return SourceSwitchResult{};
236}
237
239 QMacScreenCaptureKit::StreamId incomingStreamId,
240 QVideoFrameFormat const &format)
241{
242 Q_ASSERT(thread()->isCurrentThread());
243 if (activeStreamId() == incomingStreamId)
244 m_videoFrameFormat = format;
245}
246
248 QMacScreenCaptureKit::StreamId incomingStreamId,
249 const QString &err)
250{
251 Q_ASSERT(thread()->isCurrentThread());
252
253 qCDebug(qLcMacScreenCapture)
254 << "Stream with ID"
255 << static_cast<int64_t>(incomingStreamId)
256 << "stopped with error:"
257 << err;
258
259 if (activeStreamId() != incomingStreamId)
260 return;
261
262 // Possible improvement may be to propagate signal up to QVideoSource
263 // that we are no longer active.
264 m_activeData.reset();
265 m_videoFrameFormat.reset();
266
267 QPlatformSurfaceCapture::updateError(
268 Error::CaptureFailed,
269 u"The capture stream was closed by the system"_s);
270}
271
273{
274 Q_ASSERT(thread()->isCurrentThread());
275
276 if (m_videoFrameFormat)
277 return *m_videoFrameFormat;
278 return {};
279}
280
282{
283 Q_ASSERT(thread()->isCurrentThread());
284 return AV_PIX_FMT_VIDEOTOOLBOX;
285}
286
291
292} // namespace QFFmpeg
293
294QT_END_NAMESPACE
295
296#include "moc_qsckwindowcapture_p.cpp"
void onStreamStoppedWithErrorEvent(QMacScreenCaptureKit::StreamId streamId, const QString &)
void setCaptureSession(QPlatformMediaCaptureSession *) override
std::optional< int > ffmpegHWPixelFormat() const override
QVideoFrameFormat frameFormat() const override
std::optional< SourceSwitchResult > switchActiveSource(const Source &newSource) override
void onNewFrameFormatReceived(QMacScreenCaptureKit::StreamId streamId, const QVideoFrameFormat &)
bool setActiveInternal(bool active) override
std::unique_ptr< QPlatformSurfaceCapture > makeQSckWindowCapture()