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
22# include <QtMultimedia/private/qmultimedia_dmabuf_support_p.h>
23#endif
24
25#include <array>
26#include <iterator>
27#include <optional>
28
29QT_BEGIN_NAMESPACE
30
31namespace QtPipeWire {
32
33using namespace Qt::StringLiterals;
34
35Q_STATIC_LOGGING_CATEGORY(lcPipewireCameraStream, "qt.multimedia.pipewire.camera.stream")
36
37namespace {
38
39// The raw formats a camera realistically produces, offered when the caller did
40// not ask for one in particular.
41constexpr std::array cameraPixelFormats{
42 SPA_VIDEO_FORMAT_NV12, SPA_VIDEO_FORMAT_YUY2, SPA_VIDEO_FORMAT_UYVY,
43 SPA_VIDEO_FORMAT_I420, SPA_VIDEO_FORMAT_RGB, SPA_VIDEO_FORMAT_BGR,
44 SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_GRAY8,
45};
46
47enum class CameraMemory : uint8_t { Auto, MemPtr, MemFd, DmaBuf };
48
49CameraMemory cameraMemoryFromEnvironment()
50{
51 const QByteArray value = qgetenv("QT_PIPEWIRE_CAMERA_MEMORY");
52 if (value.isEmpty() || value == "auto")
53 return CameraMemory::Auto;
54 if (value == "memptr")
55 return CameraMemory::MemPtr;
56 if (value == "memfd")
57 return CameraMemory::MemFd;
58 if (value == "dmabuf") {
60 return CameraMemory::DmaBuf;
61#else
62 qCWarning(lcPipewireCameraStream)
63 << "QT_PIPEWIRE_CAMERA_MEMORY=dmabuf needs PipeWire 0.3.76 or newer, using auto";
64 return CameraMemory::Auto;
65#endif
66 }
67
68 qCWarning(lcPipewireCameraStream)
69 << "unknown QT_PIPEWIRE_CAMERA_MEMORY value" << value << ", using auto";
70 return CameraMemory::Auto;
71}
72
73// The buffer type an override insists on, or nullopt when any type will do.
74[[maybe_unused]] std::optional<spa_data_type> forcedDataType(CameraMemory memory)
75{
76 switch (memory) {
77 case CameraMemory::Auto:
78 return std::nullopt;
79 case CameraMemory::MemPtr:
80 return SPA_DATA_MemPtr;
81 case CameraMemory::MemFd:
82 return SPA_DATA_MemFd;
83 case CameraMemory::DmaBuf:
84 return SPA_DATA_DmaBuf;
85 }
86 Q_UNREACHABLE_RETURN(std::nullopt);
87}
88
89// Copies the frame out of the buffer. Each frame is copied eagerly, so the
90// buffer can be requeued as soon as this returns.
91std::optional<QByteArray> copyFrameData(const spa_buffer &buffer,
92 [[maybe_unused]] bool dmaBufReadable)
93{
94 const spa_data &data = buffer.datas[0];
95
97 if (data.type == SPA_DATA_DmaBuf) {
98 // A single plane, since each plane would otherwise need its own stride. A
99 // compressed frame is a byte stream, so only raw frames depend on the layout.
100 if (buffer.n_datas != 1 || !dmaBufReadable) {
101 qCDebug(lcPipewireCameraStream)
102 << "dropping DMABUF frame: needs one plane in a linear modifier";
103 return std::nullopt;
104 }
105
106 if (!data.chunk || data.maxsize == 0)
107 return std::nullopt;
108 const size_t chunkOffset = data.chunk->offset % data.maxsize;
109 if (chunkOffset + data.chunk->size > data.maxsize) {
110 qCDebug(lcPipewireCameraStream) << "dropping DMABUF frame: chunk exceeds the buffer";
111 return std::nullopt;
112 }
113 return QtMultimediaPrivate::copyDmaBufData(
114 data.fd, off_t(data.mapoffset) + off_t(chunkOffset), data.chunk->size,
115 size_t(data.mapoffset) + data.maxsize);
116 }
117#endif
118
119 if (!data.data || !data.chunk)
120 return std::nullopt;
121 return QByteArray(static_cast<const char *>(data.data) + data.chunk->offset,
122 qsizetype(data.chunk->size));
123}
124
125} // namespace
126
137
149
151{
153 // QTBUG-129587: libpipewire 1.2.5 grew fields we do not set
154 QT_WARNING_DISABLE_GCC("-Wmissing-field-initializers")
155 QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
156 // clang-format off
157 static constexpr pw_stream_events events = {
159 .state_changed = [](void *data, pw_stream_state o, pw_stream_state s, const char *err) {
160 reinterpret_cast<QPipeWireVideoStream *>(data)->onStateChanged(o, s, err);
161 },
162 .param_changed = [](void *data, uint32_t id, const spa_pod *param) {
163 reinterpret_cast<QPipeWireVideoStream *>(data)->onParamChanged(id, param);
164 },
165 .process = [](void *data) {
166 reinterpret_cast<QPipeWireVideoStream *>(data)->onProcess();
167 },
168 };
169 // clang-format on
171
172 return events;
173}
174
175namespace {
176
177// The EnumFormat param for the requested pixel format and size. With
178// linearModifier, the modifier is restricted to the linear layout, the only one
179// a DMABUF buffer can be read through.
180const spa_pod *buildFormatParam(std::array<uint8_t, 1024> &storage,
181 QVideoFrameFormat::PixelFormat requestedPixelFormat,
182 QSize requestedSize, qreal requestedFrameRate,
183 [[maybe_unused]] bool linearModifier)
184{
185 spa_pod_builder builder = SPA_POD_BUILDER_INIT(storage.data(), uint32_t(storage.size()));
186
187 spa_pod_frame frame{};
188 spa_pod_builder_push_object(&builder, &frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
189 spa_pod_builder_add(&builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0);
190
191 switch (requestedPixelFormat) {
192 case QVideoFrameFormat::Format_Jpeg: {
193 spa_pod_builder_add(&builder, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mjpg),
194 0);
195 break;
196 }
197 default: {
198 spa_pod_builder_add(&builder, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
199 0);
200
201 const spa_video_format requestedPixelFormatSpa = toSpaVideoFormat(requestedPixelFormat);
202 switch (requestedPixelFormatSpa) {
203 case SPA_VIDEO_FORMAT_UNKNOWN: {
204 // advertise all alternatives, letting PipeWire pick one of the camera's supported formats
205 spa_pod_builder_prop(&builder, SPA_FORMAT_VIDEO_format, 0);
206 spa_pod_frame choice{};
207 spa_pod_builder_push_choice(&builder, &choice, SPA_CHOICE_Enum, 0);
208 // The first entry is both the default and one of the alternatives.
209 spa_pod_builder_id(&builder, cameraPixelFormats.front());
210 for (spa_video_format format : cameraPixelFormats)
211 spa_pod_builder_id(&builder, format);
212 spa_pod_builder_pop(&builder, &choice);
213 break;
214 }
215
216 default: {
217 spa_pod_builder_add(&builder, SPA_FORMAT_VIDEO_format,
218 SPA_POD_Id(requestedPixelFormatSpa), 0);
219 break;
220 }
221 }
222 break;
223 }
224 }
225
226 if (!requestedSize.isEmpty()) {
227 const spa_rectangle exactSize = toSpaRectangle(requestedSize);
228 spa_pod_builder_add(&builder, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&exactSize), 0);
229 } else {
230 constexpr spa_rectangle minSize{ .width = 1, .height = 1 };
231 constexpr spa_rectangle maxSize{ .width = 8192, .height = 8192 };
232
233 spa_pod_builder_add(&builder, SPA_FORMAT_VIDEO_size,
234 SPA_POD_CHOICE_RANGE_Rectangle(&maxSize, &minSize, &maxSize), 0);
235 }
236
237 // Cameras are treated as variable rate with the requested rate as the
238 // upper bound
239 constexpr spa_fraction minRate{ .num = 0, .denom = 1 };
240 const FrameRate rate = rateFromFps(requestedFrameRate > 0. ? requestedFrameRate : 30.);
241 spa_pod_builder_add(&builder, SPA_FORMAT_VIDEO_framerate,
242 SPA_POD_CHOICE_RANGE_Fraction(&rate.frac, &minRate, &rate.frac), 0);
243
245 if (linearModifier) {
246 spa_pod_frame modifierChoice{};
247 spa_pod_builder_prop(&builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY);
248 spa_pod_builder_push_choice(&builder, &modifierChoice, SPA_CHOICE_Enum, 0);
249 spa_pod_builder_long(&builder, int64_t(QtMultimediaPrivate::DrmFormatModifierLinear));
250 spa_pod_builder_pop(&builder, &modifierChoice);
251 }
252#endif
253
254 return static_cast<const spa_pod *>(spa_pod_builder_pop(&builder, &frame));
255}
256
257// The Buffers param, which restricts the memory types the stream accepts.
258const spa_pod *buildBuffersParam(std::array<uint8_t, 1024> &storage, BufferDataTypes dataTypes)
259{
260 spa_pod_builder builder = SPA_POD_BUILDER_INIT(storage.data(), uint32_t(storage.size()));
261 return static_cast<const spa_pod *>(spa_pod_builder_add_object(
262 &builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType,
263 SPA_POD_CHOICE_FLAGS_Int(uint32_t(dataTypes.to_ulong()))));
264}
265
266bool offersDmaBuf(CameraMemory memory)
267{
269 && (memory == CameraMemory::Auto || memory == CameraMemory::DmaBuf);
270}
271
272BufferDataTypes bufferDataTypes(CameraMemory memory)
273{
274 BufferDataTypes dataTypes;
275 if (memory == CameraMemory::Auto || memory == CameraMemory::MemPtr)
276 dataTypes.set(SPA_DATA_MemPtr);
277 if (memory == CameraMemory::Auto || memory == CameraMemory::MemFd)
278 dataTypes.set(SPA_DATA_MemFd);
279 if (offersDmaBuf(memory))
280 dataTypes.set(SPA_DATA_DmaBuf);
281 return dataTypes;
282}
283
284} // namespace
285
286// Memory type negotiation with the camera node has three steps:
287//
288// 1. connect(): the stream offers the formats it accepts, and the buffer memory
289// types it can receive, including DMABUF when it is supported.
290// 2. param_changed(Format): once the server has picked a format, the stream
291// decides whether it can read DMABUF frames in that format. If it cannot, it
292// withdraws DMABUF from its offer, and the server falls back to mapped memory.
293// 3. process(): frames are now delivered by the server.
294//
296{
298
299 if (!m_context.isConnected())
300 return false;
301
302 const spa_dict_item items[] = {
307 };
308
311
314 };
315 if (!m_stream) {
316 qCWarning(lcPipewireCameraStream) << "pw_stream_new failed";
317 return false;
318 }
319
320 m_streamListener = {};
322
324
326
328 std::array<uint8_t, 1024> formatStorage{};
330 std::array<const spa_pod *, 3> params{};
332
333 if (offersDmaBuf(memory)) {
336 /* linearModifier */ true);
337 }
338 if (memory != CameraMemory::DmaBuf) {
341 /* linearModifier */ false);
342 }
344
345 const int status = pw_stream_connect(
349 if (status < 0) {
351 << "pw_stream_connect failed:" << make_error_code(-status).message();
353 m_stream = {};
354 return false;
355 }
356
357 return true;
358 });
359}
360
366
368 const char *error)
369{
370 qCDebug(lcPipewireCameraStream) << "stream state" << pw_stream_state_as_string(old) << "->"
372 << (error ? QString::fromUtf8(error) : QString());
373
376 : u"The PipeWire camera stream failed"_s);
377 }
378}
379
381{
382 if (!param || id != SPA_PARAM_Format)
383 return;
384
387 return;
388
390 return;
391
392 QSize size;
396 bool fixationRequired = false;
397#endif
398
399 switch (info.media_subtype) {
402 return;
406
411
413 if (fixationRequired) {
414 // Answer with our one supported modifier; triggers another
415 // param_changed with the final, fixated format.
417 << "modifier fixation requested; confirming linear support";
419 }
420#endif
421 break;
422 }
425 return;
430 m_dmaBufReadable = true;
431#endif
432 break;
433 }
434 default:
435 return;
436 }
437
440 // A DMABUF that is not linear cannot be read here. Where mapped memory is
441 // also on offer, renegotiate without DMABUF rather than dropping every frame.
442 const bool mappedOnOffer =
446 std::array<uint8_t, 1024> buffersBuffer{};
447 const spa_pod *params[] = {
449 };
451 }
452 }
453#endif
454
456 << "negotiated" << pixelFormat << size << framerate.num << "/" << framerate.denom;
457
460 << "the server picked a pixel format we cannot represent, media subtype"
462 Q_EMIT errorOccurred(u"The PipeWire camera stream picked an unsupported pixel format"_s);
463 }
464
466 if (framerate.denom != 0)
468
471}
472
475{
477 QT_WARNING_DISABLE_GCC("-Wmissing-field-initializers")
478 QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
479
480 std::array<uint8_t, 1024> buffer{};
482
485
490
491 // Single-value choice, DONT_FIXATE not set: "of the modifiers you
492 // offered, this is the one I can import."
498 const auto *pod = static_cast<const spa_pod *>(spa_pod_builder_pop(&builder, &frame));
499
501
502 const spa_pod *params[] = { pod };
504}
505#endif
506
508{
510 if (!pwBuffer) {
511 // The producer is ahead of us; dropping is better than blocking the
512 // graph.
513 qCDebug(lcPipewireCameraStream) << "no buffer available";
514 return;
515 }
516
518
520
521 if (frameFormat.isValid() && buffer->n_datas > 0) {
522 // LATER: zero-copy dmaBuf import path
525 << "frame data type" << spa_data_type(buffer->datas[0].type)
527
528 const spa_chunk *chunk = buffer->datas[0].chunk;
529
530 // A compressed format has no fixed stride; guessing one from the
531 // chunk size would be meaningless.
532 int stride = chunk->stride;
534 const int height = frameFormat.frameHeight();
535 if (height > 0)
536 stride = int(chunk->size / quint32(height));
537 }
538
541
542 if (const auto *header = static_cast<const spa_meta_header *>(spa_buffer_find_meta_data(
544 // pts is in nanoseconds since an arbitrary epoch.
546 }
547
549 }
550 }
551
553}
554
555} // namespace QtPipeWire
556
557QT_END_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink")
#define __has_include(x)
#define QT_PIPEWIRE_HAS_DMABUF_CAMERA