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
qpipewire_videostream.cpp
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 <QtMultimedia/private/qmemoryvideobuffer_p.h>
7#include <QtMultimedia/private/qpipewire_propertydict_p.h>
8#include <QtMultimedia/private/qpipewire_videoformat_support_p.h>
9#include <QtMultimedia/private/qvideoframe_p.h>
10#include <QtCore/qloggingcategory.h>
11
12#include <spa/param/video/format-utils.h>
13#include <spa/pod/builder.h>
14
15#if __has_include(<spa/param/buffers.h>)
16# include <spa/param/buffers.h>
17#else
18# include <QtMultimedia/private/qpipewire_spa_compat_p.h>
19#endif
20
21#include <array>
22
23QT_BEGIN_NAMESPACE
24
25namespace QtPipeWire {
26
27using namespace Qt::StringLiterals;
28
29Q_STATIC_LOGGING_CATEGORY(lcPipewireCameraStream, "qt.multimedia.pipewire.camera.stream")
30
31namespace {
32
33// The raw formats a camera realistically produces, offered when the caller did
34// not ask for one in particular.
35constexpr std::array cameraPixelFormats{
36 SPA_VIDEO_FORMAT_NV12, SPA_VIDEO_FORMAT_YUY2, SPA_VIDEO_FORMAT_UYVY,
37 SPA_VIDEO_FORMAT_I420, SPA_VIDEO_FORMAT_RGB, SPA_VIDEO_FORMAT_BGR,
38 SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_GRAY8,
39};
40
41} // namespace
42
53
65
67{
69 // QTBUG-129587: libpipewire 1.2.5 grew fields we do not set
70 QT_WARNING_DISABLE_GCC("-Wmissing-field-initializers")
71 QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
72 // clang-format off
73 static constexpr pw_stream_events events = {
75 .state_changed = [](void *data, pw_stream_state o, pw_stream_state s, const char *err) {
76 reinterpret_cast<QPipeWireVideoStream *>(data)->onStateChanged(o, s, err);
77 },
78 .param_changed = [](void *data, uint32_t id, const spa_pod *param) {
79 reinterpret_cast<QPipeWireVideoStream *>(data)->onParamChanged(id, param);
80 },
81 .process = [](void *data) {
82 reinterpret_cast<QPipeWireVideoStream *>(data)->onProcess();
83 },
84 };
85 // clang-format on
87
88 return events;
89}
90
92{
94
96 return false;
97
98 const spa_dict_item items[] = {
103 };
104
107
110 };
111 if (!m_stream) {
112 qCWarning(lcPipewireCameraStream) << "pw_stream_new failed";
113 return false;
114 }
115
116 m_streamListener = {};
118
119 std::array<uint8_t, 1024> buffer{};
120 spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
121
125
126 switch (m_requestedPixelFormat) {
130 break;
131 }
132 default: {
135
137 switch (requestedPixelFormatSpa) {
139 // advertise all alternatives, letting PipeWire pick one of the camera's supported formats
143 // The first entry is both the default and one of the alternatives.
148 break;
149 }
150
151 default: {
154 break;
155 }
156 }
157 break;
158 }
159 }
160
161 if (!m_requestedSize.isEmpty()) {
164 } else {
165 constexpr spa_rectangle minSize{ .width = 1, .height = 1 };
166 constexpr spa_rectangle maxSize{ .width = 8192, .height = 8192 };
167
170 }
171
172 // Cameras are treated as variable rate with the requested rate as the
173 // upper bound
174 constexpr spa_fraction minRate{ .num = 0, .denom = 1 };
178
179 // Advertise MemFd alongside the default MemPtr: a real V4L2 camera
180 // node hands out memfd-backed buffers, and without this the link
181 // fails with "error use input buffers: -22" while a synthetic
182 // source (e.g. a libcamera test node) that happens to use MemPtr
183 // still works, masking the problem.
190
191 const spa_pod *params[] = {
192 static_cast<const spa_pod *>(spa_pod_builder_pop(&builder, &frame)),
193 static_cast<const spa_pod *>(spa_pod_builder_pop(&builder, &buffersFrame)),
194 };
195
196 const int status = pw_stream_connect(
199 2);
200 if (status < 0) {
202 << "pw_stream_connect failed:" << make_error_code(-status).message();
204 m_stream = {};
205 return false;
206 }
207
208 return true;
209 });
210}
211
217
219 const char *error)
220{
221 qCDebug(lcPipewireCameraStream) << "stream state" << pw_stream_state_as_string(old) << "->"
223 << (error ? QString::fromUtf8(error) : QString());
224
227 : u"The PipeWire camera stream failed"_s);
228 }
229}
230
232{
233 if (!param || id != SPA_PARAM_Format)
234 return;
235
238 return;
239
241 return;
242
243 QSize size;
246
247 switch (info.media_subtype) {
250 return;
254 break;
255 }
258 return;
262 break;
263 }
264 default:
265 return;
266 }
267
269 << "negotiated" << pixelFormat << size << framerate.num << "/" << framerate.denom;
270
273 << "the server picked a pixel format we cannot represent, media subtype"
275 Q_EMIT errorOccurred(u"The PipeWire camera stream picked an unsupported pixel format"_s);
276 }
277
279 if (framerate.denom != 0)
281
284}
285
287{
289 if (!pwBuffer) {
290 // The producer is ahead of us; dropping is better than blocking the
291 // graph.
292 qCDebug(lcPipewireCameraStream) << "no buffer available";
293 return;
294 }
295
297
299
300 if (frameFormat.isValid() && buffer->n_datas > 0 && buffer->datas[0].data) {
301 const spa_chunk *chunk = buffer->datas[0].chunk;
302
303 // A compressed format has no fixed stride; guessing one from the
304 // chunk size would be meaningless.
305 int stride = chunk->stride;
307 const int height = frameFormat.frameHeight();
308 if (height > 0)
309 stride = int(chunk->size / quint32(height));
310 }
311
312 QByteArray data{ static_cast<const char *>(buffer->datas[0].data) + chunk->offset,
313 qsizetype(chunk->size) };
314
317
318 if (const auto *header = static_cast<const spa_meta_header *>(
320 // pts is in nanoseconds since an arbitrary epoch.
322 }
323
325 }
326
328}
329
330} // namespace QtPipeWire
331
332QT_END_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink")
#define __has_include(x)