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
4
#
include
"qpipewire_videostream_p.h"
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
23
QT_BEGIN_NAMESPACE
24
25
namespace
QtPipeWire
{
26
27
using
namespace
Qt
::
StringLiterals
;
28
29
Q_STATIC_LOGGING_CATEGORY
(
lcPipewireCameraStream
,
"qt.multimedia.pipewire.camera.stream"
)
30
31
namespace
{
32
33
// The raw formats a camera realistically produces, offered when the caller did
34
// not ask for one in particular.
35
constexpr
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
43
QPipeWireVideoStream
::
QPipeWireVideoStream
(
QCameraContextManager
&
context
,
QByteArray
targetObject
,
44
QVideoFrameFormat
::
PixelFormat
requestedPixelFormat
,
45
QSize
requestedSize
,
qreal
requestedFrameRate
)
46
:
m_context
(
context
),
47
m_targetObject
(
std
::
move
(
targetObject
)),
48
m_requestedPixelFormat
(
requestedPixelFormat
),
49
m_requestedSize
(
requestedSize
),
50
m_requestedFrameRate
(
requestedFrameRate
)
51
{
52
}
53
54
QPipeWireVideoStream
::~
QPipeWireVideoStream
()
55
{
56
if
(!
m_stream
)
57
return
;
58
59
m_context
.
pwInstance
().
runWithEventLoopLock
([&] {
60
pw_stream_disconnect
(
m_stream
.
get
());
61
spa_hook_remove
(&
m_streamListener
);
62
m_stream
= {};
63
});
64
}
65
66
const
pw_stream_events
&
QPipeWireVideoStream
::
streamEvents
()
67
{
68
QT_WARNING_PUSH
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
= {
74
.
version
=
PW_VERSION_STREAM_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
86
QT_WARNING_POP
87
88
return
events
;
89
}
90
91
bool
QPipeWireVideoStream
::
start
()
92
{
93
Q_ASSERT
(!
m_stream
);
94
95
if
(!
m_context
.
isConnected
())
96
return
false
;
97
98
const
spa_dict_item
items
[] = {
99
SPA_DICT_ITEM_INIT
(
PW_KEY_MEDIA_TYPE
,
"Video"
),
100
SPA_DICT_ITEM_INIT
(
PW_KEY_MEDIA_CATEGORY
,
"Capture"
),
101
SPA_DICT_ITEM_INIT
(
PW_KEY_MEDIA_ROLE
,
"Camera"
),
102
SPA_DICT_ITEM_INIT
(
PW_KEY_TARGET_OBJECT
,
m_targetObject
.
constData
()),
103
};
104
105
return
m_context
.
pwInstance
().
runWithEventLoopLock
([&] {
106
PwPropertiesHandle
props
=
makeProperties
(
items
);
107
108
m_stream
=
PwStreamHandle
{
109
pw_stream_new
(
m_context
.
coreConnection
().
get
(),
"qt-camera"
,
props
.
release
()),
110
};
111
if
(!
m_stream
) {
112
qCWarning
(
lcPipewireCameraStream
) <<
"pw_stream_new failed"
;
113
return
false
;
114
}
115
116
m_streamListener
= {};
117
pw_stream_add_listener
(
m_stream
.
get
(), &
m_streamListener
, &
streamEvents
(),
this
);
118
119
std
::
array
<
uint8_t
, 1024>
buffer
{};
120
spa_pod_builder
builder
= SPA_POD_BUILDER_INIT(
buffer
.
data
(),
buffer
.
size
());
121
122
spa_pod_frame
frame
{};
123
spa_pod_builder_push_object
(&
builder
, &
frame
,
SPA_TYPE_OBJECT_Format
,
SPA_PARAM_EnumFormat
);
124
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_mediaType
,
SPA_POD_Id
(
SPA_MEDIA_TYPE_video
), 0);
125
126
switch
(
m_requestedPixelFormat
) {
127
case
QVideoFrameFormat
::
Format_Jpeg
: {
128
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_mediaSubtype
,
129
SPA_POD_Id
(
SPA_MEDIA_SUBTYPE_mjpg
), 0);
130
break
;
131
}
132
default
: {
133
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_mediaSubtype
,
134
SPA_POD_Id
(
SPA_MEDIA_SUBTYPE_raw
), 0);
135
136
spa_video_format
requestedPixelFormatSpa
=
toSpaVideoFormat
(
m_requestedPixelFormat
);
137
switch
(
requestedPixelFormatSpa
) {
138
case
SPA_VIDEO_FORMAT_UNKNOWN
: {
139
// advertise all alternatives, letting PipeWire pick one of the camera's supported formats
140
spa_pod_builder_prop
(&
builder
,
SPA_FORMAT_VIDEO_format
, 0);
141
spa_pod_frame
choice
{};
142
spa_pod_builder_push_choice
(&
builder
, &
choice
,
SPA_CHOICE_Enum
, 0);
143
// The first entry is both the default and one of the alternatives.
144
spa_pod_builder_id
(&
builder
,
cameraPixelFormats
.
front
());
145
for
(
spa_video_format
format
:
cameraPixelFormats
)
146
spa_pod_builder_id
(&
builder
,
format
);
147
spa_pod_builder_pop
(&
builder
, &
choice
);
148
break
;
149
}
150
151
default
: {
152
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_VIDEO_format
,
153
SPA_POD_Id
(
toSpaVideoFormat
(
m_requestedPixelFormat
)), 0);
154
break
;
155
}
156
}
157
break
;
158
}
159
}
160
161
if
(!
m_requestedSize
.
isEmpty
()) {
162
const
spa_rectangle
exactSize
=
toSpaRectangle
(
m_requestedSize
);
163
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_VIDEO_size
,
SPA_POD_Rectangle
(&
exactSize
), 0);
164
}
else
{
165
constexpr
spa_rectangle
minSize
{ .
width
= 1, .
height
= 1 };
166
constexpr
spa_rectangle
maxSize
{ .
width
= 8192, .
height
= 8192 };
167
168
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_VIDEO_size
,
169
SPA_POD_CHOICE_RANGE_Rectangle
(&
maxSize
, &
minSize
, &
maxSize
), 0);
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 };
175
const
FrameRate
rate
=
rateFromFps
(
m_requestedFrameRate
> 0. ?
m_requestedFrameRate
: 30.);
176
spa_pod_builder_add
(&
builder
,
SPA_FORMAT_VIDEO_framerate
,
177
SPA_POD_CHOICE_RANGE_Fraction
(&
rate
.
frac
, &
minRate
, &
rate
.
frac
), 0);
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.
184
spa_pod_frame
buffersFrame
{};
185
spa_pod_builder_push_object
(&
builder
, &
buffersFrame
,
SPA_TYPE_OBJECT_ParamBuffers
,
186
SPA_PARAM_Buffers
);
187
spa_pod_builder_add
(
188
&
builder
,
SPA_PARAM_BUFFERS_dataType
,
189
SPA_POD_CHOICE_FLAGS_Int
((1u <<
SPA_DATA_MemPtr
) | (1u <<
SPA_DATA_MemFd
)), 0);
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
(
197
m_stream
.
get
(),
PW_DIRECTION_INPUT
,
PW_ID_ANY
,
198
pw_stream_flags
(
PW_STREAM_FLAG_AUTOCONNECT
|
PW_STREAM_FLAG_MAP_BUFFERS
),
params
,
199
2);
200
if
(
status
< 0) {
201
qCWarning
(
lcPipewireCameraStream
)
202
<<
"pw_stream_connect failed:"
<<
make_error_code
(-
status
).
message
();
203
spa_hook_remove
(&
m_streamListener
);
204
m_stream
= {};
205
return
false
;
206
}
207
208
return
true
;
209
});
210
}
211
212
QVideoFrameFormat
QPipeWireVideoStream
::
frameFormat
()
const
213
{
214
std
::
lock_guard
guard
{
m_formatMutex
};
215
return
m_frameFormat
;
216
}
217
218
void
QPipeWireVideoStream
::
onStateChanged
(
pw_stream_state
old
,
pw_stream_state
state
,
219
const
char
*
error
)
220
{
221
qCDebug
(
lcPipewireCameraStream
) <<
"stream state"
<<
pw_stream_state_as_string
(
old
) <<
"->"
222
<<
pw_stream_state_as_string
(
state
)
223
<< (
error
?
QString
::
fromUtf8
(
error
) :
QString
());
224
225
if
(
state
==
PW_STREAM_STATE_ERROR
) {
226
emit
errorOccurred
(
error
?
QString
::
fromUtf8
(
error
)
227
: u"The PipeWire camera stream failed"_s);
228
}
229
}
230
231
void
QPipeWireVideoStream
::
onParamChanged
(
uint32_t
id
,
const
spa_pod
*
param
)
232
{
233
if
(!
param
||
id
!=
SPA_PARAM_Format
)
234
return
;
235
236
spa_video_info
info
{};
237
if
(
spa_format_parse
(
param
, &
info
.
media_type
, &
info
.
media_subtype
) < 0)
238
return
;
239
240
if
(
info
.
media_type
!=
SPA_MEDIA_TYPE_video
)
241
return
;
242
243
QSize
size
;
244
QVideoFrameFormat
::
PixelFormat
pixelFormat
=
QVideoFrameFormat
::
Format_Invalid
;
245
spa_fraction
framerate
{};
246
247
switch
(
info
.
media_subtype
) {
248
case
SPA_MEDIA_SUBTYPE_raw
: {
249
if
(
spa_format_video_raw_parse
(
param
, &
info
.
info
.
raw
) < 0)
250
return
;
251
size
=
QSize
(
int
(
info
.
info
.
raw
.
size
.
width
),
int
(
info
.
info
.
raw
.
size
.
height
));
252
pixelFormat
=
toQtPixelFormat
(
info
.
info
.
raw
.
format
);
253
framerate
=
info
.
info
.
raw
.
framerate
;
254
break
;
255
}
256
case
SPA_MEDIA_SUBTYPE_mjpg
: {
257
if
(
spa_format_video_mjpg_parse
(
param
, &
info
.
info
.
mjpg
) < 0)
258
return
;
259
size
=
QSize
(
int
(
info
.
info
.
mjpg
.
size
.
width
),
int
(
info
.
info
.
mjpg
.
size
.
height
));
260
pixelFormat
=
QVideoFrameFormat
::
Format_Jpeg
;
261
framerate
=
info
.
info
.
mjpg
.
framerate
;
262
break
;
263
}
264
default
:
265
return
;
266
}
267
268
qCDebug
(
lcPipewireCameraStream
)
269
<<
"negotiated"
<<
pixelFormat
<<
size
<<
framerate
.
num
<<
"/"
<<
framerate
.
denom
;
270
271
if
(
pixelFormat
==
QVideoFrameFormat
::
Format_Invalid
) {
272
qCWarning
(
lcPipewireCameraStream
)
273
<<
"the server picked a pixel format we cannot represent, media subtype"
274
<<
info
.
media_subtype
;
275
Q_EMIT
errorOccurred
(u"The PipeWire camera stream picked an unsupported pixel format"_s);
276
}
277
278
QVideoFrameFormat
frameFormat
(
size
,
pixelFormat
);
279
if
(
framerate
.
denom
!= 0)
280
frameFormat
.
setStreamFrameRate
(
qreal
(
framerate
.
num
) /
qreal
(
framerate
.
denom
));
281
282
std
::
lock_guard
guard
{
m_formatMutex
};
283
m_frameFormat
=
std
::
move
(
frameFormat
);
284
}
285
286
void
QPipeWireVideoStream
::
onProcess
()
287
{
288
pw_buffer
*
pwBuffer
=
pw_stream_dequeue_buffer
(
m_stream
.
get
());
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
296
const
QVideoFrameFormat
frameFormat
=
this
->
frameFormat
();
297
298
spa_buffer
*
buffer
=
pwBuffer
->
buffer
;
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
;
306
if
(
stride
== 0 &&
frameFormat
.
pixelFormat
() !=
QVideoFrameFormat
::
Format_Jpeg
) {
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
315
QVideoFrame
frame
=
QVideoFramePrivate
::
createFrame
(
316
std
::
make_unique
<
QMemoryVideoBuffer
>(
std
::
move
(
data
),
stride
),
frameFormat
);
317
318
if
(
const
auto
*
header
=
static_cast
<
const
spa_meta_header
*>(
319
spa_buffer_find_meta_data
(
buffer
,
SPA_META_Header
,
sizeof
(
spa_meta_header
)))) {
320
// pts is in nanoseconds since an arbitrary epoch.
321
frame
.
setStartTime
(
qint64
(
header
->
pts
) / 1000);
322
}
323
324
emit
newVideoFrame
(
frame
);
325
}
326
327
pw_stream_queue_buffer
(
m_stream
.
get
(),
pwBuffer
);
328
}
329
330
}
// namespace QtPipeWire
331
332
QT_END_NAMESPACE
QtPipeWire
Definition
qpipewire_async_support.cpp:10
QtPipeWire::Q_STATIC_LOGGING_CATEGORY
Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink")
std
[33]
Definition
src_corelib_tools_qhash.cpp:421
__has_include
#define __has_include(x)
Definition
qcompilerdetection.h:459
qtmultimedia
src
multimedia
pipewire
qpipewire_videostream.cpp
Generated on
for Qt by
1.16.1