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_audiocontextmanager.cpp
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
5
10
11#include <QtCore/qcoreapplication.h>
12#include <QtCore/qdebug.h>
13#include <QtCore/qsemaphore.h>
14
15#include <pipewire/extensions/metadata.h>
16
17#include <system_error>
18
19#if __has_include(<spa/utils/json.h>)
20# include <spa/utils/json.h>
21#else
22# include <QtCore/qjsondocument.h>
23# include <QtCore/qjsonvalue.h>
24#endif
25
26#if !PW_CHECK_VERSION(0, 3, 75)
27extern "C" {
28bool pw_check_library_version(int major, int minor, int micro);
29}
30#endif
31
32QT_BEGIN_NAMESPACE
33
34namespace QtPipeWire {
35
37
38Q_STATIC_LOGGING_CATEGORY(lcPipewireRegistry, "qt.multimedia.pipewire.registry");
39
43 },
46 }
47{
48 prepareEventLoop();
49 prepareContext();
50 if (!m_context) {
51 // pipewire server not available
52 return;
53 }
54
55 connectToPipewireInstance();
56 if (!isConnected())
57 return;
58
59 startDeviceMonitor();
60 startEventLoop();
61}
62
64{
65 if (isConnected()) {
66 stopEventLoop();
67 stopActiveStreams();
68 }
69
70 m_deviceMonitor.reset();
71 m_registry.reset();
72 m_coreConnection.reset();
73 m_context.reset();
74 m_eventLoop.reset();
75}
76
78{
79 return pw_check_library_version(0, 3, 44); // we require PW_KEY_OBJECT_SERIAL
80}
81
83{
84 return s_audioContextInstance;
85}
86
88{
89 return bool(m_coreConnection);
90}
91
93{
94 return *instance()->m_deviceMonitor;
95}
96
98{
99 return pw_thread_loop_in_thread(instance()->m_eventLoop.get());
100}
101
103{
104 return pw_thread_loop_get_loop(instance()->m_eventLoop.get());
105}
106
108{
109 return PwNodeHandle{
110 reinterpret_cast<pw_node *>(pw_registry_bind(m_registry.get(), id.value,
111 PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
112 sizeof(void *))),
113 };
114}
115
117{
118 return PwMetadataHandle{
119 reinterpret_cast<pw_metadata *>(pw_registry_bind(m_registry.get(), id.value,
120 PW_TYPE_INTERFACE_Metadata,
121 PW_VERSION_METADATA, sizeof(void *))),
122 };
123}
124
126{
127 using namespace std::chrono_literals;
128 CoreEventSyncHelper syncHelper;
129
130 auto syncOrErr = syncHelper.sync(m_coreConnection.get(), 3s);
131 if (syncOrErr == true)
132 return;
133 if (syncOrErr == false)
134 qWarning() << "pw_core_sync timed out";
135 else if (syncOrErr.error()) {
136 int err = syncOrErr.error();
137 qWarning() << "CoreEventSyncHelper::sync failed:" << make_error_code(err).message();
138 }
139}
140
142{
143 std::lock_guard guard{ m_activeStreamMutex };
144 m_activeStreams.emplace(std::move(stream));
145}
146
148 const std::shared_ptr<QPipewireAudioStream> &stream)
149{
150 std::lock_guard guard{ m_activeStreamMutex };
151 m_activeStreams.erase(stream);
152}
153
155{
156 return m_coreConnection;
157}
158
159void QAudioContextManager::prepareEventLoop()
160{
161 m_eventLoop = PwThreadLoopHandle{
162 pw_thread_loop_new("QAudioContext", /*props=*/nullptr),
163 };
164 if (!m_eventLoop) {
165 qFatal() << "Failed to create pipewire main loop" << make_error_code().message();
166 return;
167 }
168}
169
170void QAudioContextManager::startEventLoop()
171{
172 int status = pw_thread_loop_start(m_eventLoop.get());
173 if (status < 0)
174 qFatal() << "Failed to start event loop" << make_error_code(-status).message();
175}
176
177void QAudioContextManager::stopEventLoop()
178{
179 pw_thread_loop_stop(m_eventLoop.get());
180}
181
182void QAudioContextManager::prepareContext()
183{
184 PwPropertiesHandle props = makeProperties({
185 { PW_KEY_APP_NAME, qApp->applicationName().toUtf8().data() },
186 });
187
188 Q_ASSERT(m_eventLoop);
189 m_context = PwContextHandle{
190 pw_context_new(pw_thread_loop_get_loop(m_eventLoop.get()), props.release(),
191 /*user_data_size=*/0),
192 };
193}
194
195void QAudioContextManager::connectToPipewireInstance()
196{
197 Q_ASSERT(m_eventLoop && m_context);
198 m_coreConnection = PwCoreConnectionHandle{
199 pw_context_connect(m_context.get(), /*props=*/nullptr,
200 /*user_data_size=*/0),
201 };
202
203 if (!m_coreConnection)
204 qInfo() << "Failed to connect to pipewire instance" << make_error_code().message();
205}
206
207void QAudioContextManager::objectAddedCb(void *data, uint32_t id, uint32_t permissions,
208 const char *type, uint32_t version, const spa_dict *props)
209{
210 Q_ASSERT(isInPwThreadLoop());
211
212 qCDebug(lcPipewireRegistry) << "objectAdded" << id << QString::number(permissions, 8) << type
213 << version << *props;
214
215 std::optional<PipewireRegistryType> objectType = parsePipewireRegistryType(type);
216 if (!objectType) {
217 qCritical() << "object type cannot be parsed:" << type;
218 return;
219 }
220
221 if (!props) {
222 qCritical() << "null property received";
223 return;
224 }
225
226 reinterpret_cast<QAudioContextManager *>(data)->objectAdded(ObjectId{ id }, permissions,
227 *objectType, version, *props);
228}
229
230void QAudioContextManager::objectRemovedCb(void *data, uint32_t id)
231{
232 Q_ASSERT(isInPwThreadLoop());
233
234 qCDebug(lcPipewireRegistry) << "objectRemoved" << id;
235
236 auto *self = reinterpret_cast<QAudioContextManager *>(data);
237 self->objectRemoved(ObjectId{ id });
238}
239
240void QAudioContextManager::objectAdded(ObjectId id, uint32_t permissions, PipewireRegistryType type,
241 uint32_t version, const spa_dict &props)
242{
243 switch (type) {
244 case PipewireRegistryType::Device:
245 case PipewireRegistryType::Node:
246 return m_deviceMonitor->objectAdded(id, permissions, type, version, props);
247
249 const char *name = spa_dict_lookup(&props, PW_KEY_METADATA_NAME);
250 if (name == std::string_view("default"))
251 // the "default" metadata will inform us about the "default" device
252 return startListenDefaultMetadataObject(id, version);
253 return;
254 }
255
256 default:
257 return;
258 }
259}
260
261void QAudioContextManager::objectRemoved(ObjectId id)
262{
263 m_deviceMonitor->objectRemoved(id);
264}
265
266void QAudioContextManager::startListenDefaultMetadataObject(ObjectId id, uint32_t version)
267{
268 if (m_defaultMetadataObject) {
269 qWarning(lcPipewireRegistry) << "metadata already registered";
270 return;
271 }
272
273 if (version < PW_VERSION_METADATA) {
274 Q_UNLIKELY_BRANCH;
275 qWarning(lcPipewireRegistry)
276 << "metadata version too old, cannot listen to default metadata object";
277 return;
278 }
279
280 static constexpr pw_metadata_events metadata_events = {
281 .version = PW_VERSION_METADATA_EVENTS,
282 .property = [](void *data, uint32_t subject, const char *key, const char *type,
283 const char *value) -> int {
284 Q_ASSERT(subject == PW_ID_CORE);
285
286 auto *self = reinterpret_cast<QAudioContextManager *>(data);
287
288 Q_ASSERT(key);
289 return self->handleDefaultMetadataObjectEvent(MetadataRecord{
290 .key = key,
291 .type = type,
292 .value = value,
293 });
294 },
295 };
296
297 m_defaultMetadataObject = bindMetadata(id);
298 if (!m_defaultMetadataObject) {
299 qFatal() << "cannot bind to metadata";
300 return;
301 }
302
303 int status = pw_metadata_add_listener(m_defaultMetadataObject.get(),
304 &m_defaultMetadataObjectListener, &metadata_events, this);
305 if (status < 0)
306 qFatal() << "Failed to add listener" << make_error_code(-status).message();
307}
308
309namespace {
310
311// parse json object with one "name" member
312std::optional<QByteArray> jsonParseObjectName(const char *json_str)
313{
314#if __has_include(<spa/utils/json.h>)
315 using namespace std::string_view_literals;
316
317 struct spa_json json;
318 spa_json_init(&json, json_str, strlen(json_str));
319
320 struct spa_json it;
321 if (spa_json_enter_object(&json, &it) > 0) {
322 char key[256];
323 while (spa_json_get_string(&it, key, sizeof(key)) > 0) {
324 if (key == "name"sv) {
325 char value[16384];
326 if (spa_json_get_string(&it, value, sizeof(value)) >= 0)
327 return QByteArray{ value };
328 } else {
329 spa_json_next(&it, nullptr);
330 }
331 }
332 }
333
334 return std::nullopt;
335#else
336 // old pipewire does not provide json.h, so we use Qt to parse
337
338 using namespace Qt::Literals;
339
340 QByteArray value{ json_str };
341 QJsonDocument doc = QJsonDocument::fromJson(value);
342 if (doc.isNull()) {
343 qWarning() << "JSON parse error:" << json_str;
344 return std::nullopt;
345 }
346
347 QJsonValue name = doc[u"name"_s];
348 if (!name.isString())
349 return std::nullopt;
350 return name.toString().toUtf8();
351#endif
352}
353
354} // namespace
355
356int QAudioContextManager::handleDefaultMetadataObjectEvent(const MetadataRecord &record)
357{
358 using namespace std::string_view_literals;
359
360 qDebug(lcPipewireRegistry) << "metadata:" << record.key << record.type << record.value;
361
362 if (record.key == nullptr) {
363 // "NULL clears all metadata for the subject"
364 m_deviceMonitor->setDefaultAudioSource(QAudioDeviceMonitor::NoDefaultDevice);
365 m_deviceMonitor->setDefaultAudioSink(QAudioDeviceMonitor::NoDefaultDevice);
366 return 0;
367 }
368
369 auto extractName = [&]() -> std::optional<QByteArray> {
370 if (record.type != "Spa:String:JSON"sv)
371 return std::nullopt;
372 return jsonParseObjectName(record.value);
373 };
374
375 if (record.key == "default.audio.source"sv) {
376 if (record.value) {
377 std::optional<QByteArray> name = extractName();
378 if (name)
379 m_deviceMonitor->setDefaultAudioSource(std::move(*name));
380 } else {
381 m_deviceMonitor->setDefaultAudioSource(QAudioDeviceMonitor::NoDefaultDevice);
382 }
383
384 return 0;
385 }
386
387 if (record.key == "default.audio.sink"sv) {
388 if (record.value) {
389 std::optional<QByteArray> name = extractName();
390 if (name)
391 m_deviceMonitor->setDefaultAudioSink(std::move(*name));
392 } else {
393 m_deviceMonitor->setDefaultAudioSink(QAudioDeviceMonitor::NoDefaultDevice);
394 }
395 return 0;
396 }
397
398 return 0;
399}
400
401void QAudioContextManager::stopActiveStreams()
402{
403 auto streams = std::exchange(m_activeStreams, {});
404
405 for (const auto &stream : streams)
406 stream->resetStream();
407}
408
409void QAudioContextManager::startDeviceMonitor()
410{
411 m_registry = PwRegistryHandle{
412 pw_core_get_registry(m_coreConnection.get(), PW_VERSION_REGISTRY,
413 /*user_data_size=*/sizeof(QAudioContextManager *)),
414 };
415 if (!m_registry) {
416 qFatal() << "Failed to create pipewire registry" << make_error_code().message();
417 return;
418 }
419
420 spa_zero(m_registryListener);
421
422 static constexpr struct pw_registry_events registry_events = {
423 .version = PW_VERSION_REGISTRY_EVENTS,
424 .global = QAudioContextManager::objectAddedCb,
425 .global_remove = QAudioContextManager::objectRemovedCb,
426 };
427 int status =
428 pw_registry_add_listener(m_registry.get(), &m_registryListener, &registry_events, this);
429 if (status < 0)
430 qFatal() << "Failed to add listener" << make_error_code(-status).message();
431}
432
433} // namespace QtPipeWire
434
435QT_END_NAMESPACE
void registerStreamReference(std::shared_ptr< QPipewireAudioStream >)
void unregisterStreamReference(const std::shared_ptr< QPipewireAudioStream > &)
const PwCoreConnectionHandle & coreConnection() const
Q_STATIC_LOGGING_CATEGORY(lcPipewireRegistry, "qt.multimedia.pipewire.registry")
Q_GLOBAL_STATIC(QAudioContextManager, s_audioContextInstance)
std::unique_ptr< pw_core, PwCoreConnectionDeleter > PwCoreConnectionHandle
StrongIdType< uint32_t, ObjectIdTag > ObjectId
std::error_code make_error_code(int errnoValue=errno)
#define __has_include(x)
bool pw_check_library_version(int major, int minor, int micro)