PulseAudio Qt Bindings

context.cpp
1/*
2 SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "context.h"
8#include "server.h"
9
10#include "debug.h"
11#include <QAbstractEventDispatcher>
12#include <QDBusConnection>
13#include <QDBusServiceWatcher>
14#include <QGuiApplication>
15#include <QIcon>
16#include <QTimer>
17
18#include <memory>
19
20#include "card.h"
21#include "client.h"
22#include "module.h"
23#include "sink.h"
24#include "sinkinput.h"
25#include "source.h"
26#include "sourceoutput.h"
27#include "streamrestore.h"
28
29#include "context_p.h"
30#include "server_p.h"
31#include "streamrestore_p.h"
32
33namespace PulseAudioQt
34{
36{
37 return PA_VOLUME_NORM;
38}
39
41{
42 return PA_VOLUME_MUTED;
43}
44
46{
47 return PA_VOLUME_MAX;
48}
49
51{
52 return PA_VOLUME_UI_MAX;
53}
54
55QString ContextPrivate::s_applicationId;
56
57#ifndef K_DOXYGEN
58
59namespace
60{
61constexpr auto EVENT_ROLE = "sink-input-by-media-role:event";
62} // namespace
63
64static bool isGoodState(int eol)
65{
66 if (eol < 0) {
67 // Error
68 return false;
69 }
70
71 if (eol > 0) {
72 // End of callback chain
73 return false;
74 }
75
76 return true;
77}
78
79// --------------------------
80
81static void sink_cb(pa_context *context, const pa_sink_info *info, int eol, void *data)
82{
83 if (!isGoodState(eol))
84 return;
85 Q_ASSERT(context);
86 Q_ASSERT(data);
87 static_cast<ContextPrivate *>(data)->sinkCallback(info);
88}
89
90static void sink_input_callback(pa_context *context, const pa_sink_input_info *info, int eol, void *data)
91{
92 if (!isGoodState(eol))
93 return;
94 // pulsesink probe is used by gst-pulse only to query sink formats (not for playback)
95 if (qstrcmp(info->name, "pulsesink probe") == 0) {
96 return;
97 }
98 if (const char *id = pa_proplist_gets(info->proplist, "module-stream-restore.id")) {
99 if (qstrcmp(id, EVENT_ROLE) == 0) {
100 qCDebug(PULSEAUDIOQT) << "Ignoring event role sink input.";
101 return;
102 }
103 }
104 Q_ASSERT(context);
105 Q_ASSERT(data);
106 static_cast<ContextPrivate *>(data)->sinkInputCallback(info);
107}
108
109static void source_cb(pa_context *context, const pa_source_info *info, int eol, void *data)
110{
111 if (!isGoodState(eol))
112 return;
113 // FIXME: This forces excluding monitors
114 if (info->monitor_of_sink != PA_INVALID_INDEX)
115 return;
116 Q_ASSERT(context);
117 Q_ASSERT(data);
118 static_cast<ContextPrivate *>(data)->sourceCallback(info);
119}
120
121static void source_output_cb(pa_context *context, const pa_source_output_info *info, int eol, void *data)
122{
123 if (!isGoodState(eol))
124 return;
125 // FIXME: This forces excluding these apps
126 if (const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) {
127 if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 //
128 || strcmp(app, "org.gnome.VolumeControl") == 0 //
129 || strcmp(app, "org.kde.kmixd") == 0 //
130 || strcmp(app, "org.kde.plasma-pa") == 0) //
131 return;
132 }
133 Q_ASSERT(context);
134 Q_ASSERT(data);
135 static_cast<ContextPrivate *>(data)->sourceOutputCallback(info);
136}
137
138static void client_cb(pa_context *context, const pa_client_info *info, int eol, void *data)
139{
140 if (!isGoodState(eol))
141 return;
142 Q_ASSERT(context);
143 Q_ASSERT(data);
144 static_cast<ContextPrivate *>(data)->clientCallback(info);
145}
146
147static void card_cb(pa_context *context, const pa_card_info *info, int eol, void *data)
148{
149 if (!isGoodState(eol))
150 return;
151 Q_ASSERT(context);
152 Q_ASSERT(data);
153 static_cast<ContextPrivate *>(data)->cardCallback(info);
154}
155
156static void module_info_list_cb(pa_context *context, const pa_module_info *info, int eol, void *data)
157{
158 if (!isGoodState(eol))
159 return;
160 Q_ASSERT(context);
161 Q_ASSERT(data);
162 static_cast<ContextPrivate *>(data)->moduleCallback(info);
163}
164
165static void server_cb(pa_context *context, const pa_server_info *info, void *data)
166{
167 Q_ASSERT(context);
168 Q_ASSERT(data);
169 if (!info) {
170 // info may be nullptr when e.g. the server doesn't reply in time (e.g. it is stuck)
171 // https://bugs.kde.org/show_bug.cgi?id=454647
172 qCWarning(PULSEAUDIOQT) << "server_cb() called without info!";
173 return;
174 }
175 static_cast<ContextPrivate *>(data)->serverCallback(info);
176}
177
178static void context_state_callback(pa_context *context, void *data)
179{
180 Q_ASSERT(data);
181 static_cast<ContextPrivate *>(data)->contextStateCallback(context);
182}
183
184static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data)
185{
186 Q_ASSERT(data);
187 static_cast<ContextPrivate *>(data)->subscribeCallback(context, type, index);
188}
189
190static void ext_stream_restore_read_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
191{
192 if (!isGoodState(eol)) {
193 return;
194 }
195 Q_ASSERT(context);
196 Q_ASSERT(data);
197 static_cast<ContextPrivate *>(data)->streamRestoreCallback(info);
198}
199
200static void ext_stream_restore_subscribe_cb(pa_context *context, void *data)
201{
202 Q_ASSERT(context);
203 Q_ASSERT(data);
204 if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) {
205 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read() failed";
206 }
207}
208
209static void ext_stream_restore_change_sink_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
210{
211 if (!isGoodState(eol)) {
212 return;
213 }
214 Q_ASSERT(context);
215 Q_ASSERT(data);
216 if (qstrncmp(info->name, "sink-input-by", 13) == 0) {
217 ContextPrivate *contextp = static_cast<ContextPrivate *>(data);
218 const QByteArray deviceData = contextp->m_newDefaultSink.toUtf8();
219 pa_ext_stream_restore_info newinfo;
220 newinfo.name = info->name;
221 newinfo.channel_map = info->channel_map;
222 newinfo.volume = info->volume;
223 newinfo.mute = info->mute;
224 newinfo.device = deviceData.constData();
225 contextp->streamRestoreWrite(&newinfo);
226 }
227}
228
229static void ext_stream_restore_change_source_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
230{
231 if (!isGoodState(eol)) {
232 return;
233 }
234 Q_ASSERT(context);
235 Q_ASSERT(data);
236 if (qstrncmp(info->name, "source-output-by", 16) == 0) {
237 ContextPrivate *contextp = static_cast<ContextPrivate *>(data);
238 const QByteArray deviceData = contextp->m_newDefaultSource.toUtf8();
239 pa_ext_stream_restore_info newinfo;
240 newinfo.name = info->name;
241 newinfo.channel_map = info->channel_map;
242 newinfo.volume = info->volume;
243 newinfo.mute = info->mute;
244 newinfo.device = deviceData.constData();
245 contextp->streamRestoreWrite(&newinfo);
246 }
247}
248
249#endif
250
251// --------------------------
252
253Context::Context(QObject *parent)
254 : QObject(parent)
255 , d(new ContextPrivate(this))
256{
257 connect(this, &Context::stateChanged, this, [this] {
258 qCDebug(PULSEAUDIOQT) << "context state changed:" << d->m_state;
259 });
260
261 d->m_server = new Server(this);
262 d->m_context = nullptr;
263 d->m_mainloop = nullptr;
264
265 d->connectToDaemon();
266
267 QDBusServiceWatcher *watcher =
268 new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this);
269 connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
270 d->connectToDaemon();
271 });
272
273 connect(&d->m_connectTimer, &QTimer::timeout, this, [this] {
274 d->forceDisconnect();
275 d->connectToDaemon();
276 d->checkConnectTries();
277 });
278
279 connect(&d->m_sinks, &MapBaseQObject::added, this, [this](int, QObject *object) {
280 Q_EMIT sinkAdded(static_cast<Sink *>(object));
281 });
282 connect(&d->m_sinks, &MapBaseQObject::removed, this, [this](int, QObject *object) {
283 Q_EMIT sinkRemoved(static_cast<Sink *>(object));
284 });
285
286 connect(&d->m_sinkInputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
287 Q_EMIT sinkInputAdded(static_cast<SinkInput *>(object));
288 });
289 connect(&d->m_sinkInputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
290 Q_EMIT sinkInputRemoved(static_cast<SinkInput *>(object));
291 });
292
293 connect(&d->m_sources, &MapBaseQObject::added, this, [this](int, QObject *object) {
294 Q_EMIT sourceAdded(static_cast<Source *>(object));
295 });
296 connect(&d->m_sources, &MapBaseQObject::removed, this, [this](int, QObject *object) {
297 Q_EMIT sourceRemoved(static_cast<Source *>(object));
298 });
299
300 connect(&d->m_sourceOutputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
301 Q_EMIT sourceOutputAdded(static_cast<SourceOutput *>(object));
302 });
303 connect(&d->m_sourceOutputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
304 Q_EMIT sourceOutputRemoved(static_cast<SourceOutput *>(object));
305 });
306
307 connect(&d->m_clients, &MapBaseQObject::added, this, [this](int, QObject *object) {
308 Q_EMIT clientAdded(static_cast<Client *>(object));
309 });
310 connect(&d->m_clients, &MapBaseQObject::removed, this, [this](int, QObject *object) {
311 Q_EMIT clientRemoved(static_cast<Client *>(object));
312 });
313
314 connect(&d->m_cards, &MapBaseQObject::added, this, [this](int, QObject *object) {
315 Q_EMIT cardAdded(static_cast<Card *>(object));
316 });
317 connect(&d->m_cards, &MapBaseQObject::removed, this, [this](int, QObject *object) {
318 Q_EMIT cardRemoved(static_cast<Card *>(object));
319 });
320
321 connect(&d->m_modules, &MapBaseQObject::added, this, [this](int, QObject *object) {
322 Q_EMIT moduleAdded(static_cast<Module *>(object));
323 });
324 connect(&d->m_modules, &MapBaseQObject::removed, this, [this](int, QObject *object) {
325 Q_EMIT moduleRemoved(static_cast<Module *>(object));
326 });
327
328 connect(&d->m_streamRestores, &MapBaseQObject::added, this, [this](int, QObject *object) {
329 Q_EMIT streamRestoreAdded(static_cast<StreamRestore *>(object));
330 });
331 connect(&d->m_streamRestores, &MapBaseQObject::removed, this, [this](int, QObject *object) {
332 Q_EMIT streamRestoreRemoved(static_cast<StreamRestore *>(object));
333 });
334}
335
336ContextPrivate::ContextPrivate(Context *q)
337 : q(q)
338{
339}
340
341Context::~Context()
342{
343}
344
345ContextPrivate::~ContextPrivate()
346{
347 if (m_context) {
348 pa_context_unref(m_context);
349 m_context = nullptr;
350 }
351
352 if (m_mainloop) {
353 pa_glib_mainloop_free(m_mainloop);
354 m_mainloop = nullptr;
355 }
356
357 disconnectSignals();
358 reset();
359}
360
361Context *Context::instance()
362{
363 static std::unique_ptr<Context> context(new Context);
364 return context.get();
365}
366
367void ContextPrivate::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index)
368{
369 Q_ASSERT(context == m_context);
370
371 switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
372 case PA_SUBSCRIPTION_EVENT_SINK:
373 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
374 m_sinks.removeEntry(index);
375 } else {
376 if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) {
377 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_info_by_index() failed";
378 return;
379 }
380 }
381 break;
382
383 case PA_SUBSCRIPTION_EVENT_SOURCE:
384 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
385 m_sources.removeEntry(index);
386 } else {
387 if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) {
388 qCWarning(PULSEAUDIOQT) << "pa_context_get_source_info_by_index() failed";
389 return;
390 }
391 }
392 break;
393
394 case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
395 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
396 m_sinkInputs.removeEntry(index);
397 } else {
398 if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) {
399 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info() failed";
400 return;
401 }
402 }
403 break;
404
405 case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
406 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
407 m_sourceOutputs.removeEntry(index);
408 } else {
409 if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) {
410 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info() failed";
411 return;
412 }
413 }
414 break;
415
416 case PA_SUBSCRIPTION_EVENT_CLIENT:
417 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
418 m_clients.removeEntry(index);
419 } else {
420 if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) {
421 qCWarning(PULSEAUDIOQT) << "pa_context_get_client_info() failed";
422 return;
423 }
424 }
425 break;
426
427 case PA_SUBSCRIPTION_EVENT_CARD:
428 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
429 m_cards.removeEntry(index);
430 } else {
431 if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) {
432 qCWarning(PULSEAUDIOQT) << "pa_context_get_card_info_by_index() failed";
433 return;
434 }
435 }
436 break;
437
438 case PA_SUBSCRIPTION_EVENT_MODULE:
439 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
440 m_modules.removeEntry(index);
441 } else {
442 if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) {
443 qCWarning(PULSEAUDIOQT) << "pa_context_get_module_info_list() failed";
444 return;
445 }
446 }
447 break;
448
449 case PA_SUBSCRIPTION_EVENT_SERVER:
450 if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) {
451 qCWarning(PULSEAUDIOQT) << "pa_context_get_server_info() failed";
452 return;
453 }
454 break;
455 }
456}
457
458void ContextPrivate::contextStateCallback(pa_context *c)
459{
460 pa_context_state_t state = pa_context_get_state(c);
461 qCDebug(PULSEAUDIOQT) << "state callback" << state;
462
463 m_state = [state]() -> Context::State {
464 switch (state) {
465 case PA_CONTEXT_UNCONNECTED:
466 return Context::State::Unconnected;
467 case PA_CONTEXT_CONNECTING:
468 return Context::State::Connecting;
469 case PA_CONTEXT_AUTHORIZING:
470 return Context::State::Authorizing;
471 case PA_CONTEXT_SETTING_NAME:
472 return Context::State::SettingName;
473 case PA_CONTEXT_READY:
474 return Context::State::Ready;
475 case PA_CONTEXT_FAILED:
476 return Context::State::Failed;
477 case PA_CONTEXT_TERMINATED:
478 return Context::State::Terminated;
479 }
480 return Context::State::Unconnected;
481 }();
482 // Queue state changes to avoid race conditions with changes going on below in the code. It's not time critical anyway.
483 QMetaObject::invokeMethod(q, &Context::stateChanged, Qt::QueuedConnection);
484
485 if (state == PA_CONTEXT_READY) {
486 qCDebug(PULSEAUDIOQT) << "ready, stopping connect timer";
487 m_connectTimer.stop();
488 Q_EMIT q->autoConnectingChanged();
489
490 // 1. Register for the stream changes (except during probe)
491 if (m_context == c) {
492 pa_context_set_subscribe_callback(c, subscribe_cb, this);
493
494 if (!PAOperation(
495 pa_context_subscribe(c,
496 (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_CLIENT
497 | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT
498 | PA_SUBSCRIPTION_MASK_CARD | PA_SUBSCRIPTION_MASK_MODULE | PA_SUBSCRIPTION_MASK_SERVER),
499 nullptr,
500 nullptr))) {
501 qCWarning(PULSEAUDIOQT) << "pa_context_subscribe() failed";
502 return;
503 }
504 }
505
506 if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) {
507 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_info_list() failed";
508 return;
509 }
510
511 if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) {
512 qCWarning(PULSEAUDIOQT) << "pa_context_get_source_info_list() failed";
513 return;
514 }
515
516 if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) {
517 qCWarning(PULSEAUDIOQT) << "pa_context_client_info_list() failed";
518 return;
519 }
520
521 if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) {
522 qCWarning(PULSEAUDIOQT) << "pa_context_get_card_info_list() failed";
523 return;
524 }
525
526 if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) {
527 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info_list() failed";
528 return;
529 }
530
531 if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) {
532 qCWarning(PULSEAUDIOQT) << "pa_context_get_source_output_info_list() failed";
533 return;
534 }
535
536 if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) {
537 qCWarning(PULSEAUDIOQT) << "pa_context_get_module_info_list() failed";
538 return;
539 }
540
541 if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) {
542 qCWarning(PULSEAUDIOQT) << "pa_context_get_server_info() failed";
543 return;
544 }
545
546 // Always synthesize an event stream. Neither PA nor PW have one until the first event sound is played.
547 // This stream will eventually get updated by either the read_cb or the subscription.
548 // https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/4186
549 synthesizeEventStream();
550 if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) {
551 pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this);
552 PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this));
553 } else {
554 m_streamRestores.reset();
555 qCWarning(PULSEAUDIOQT) << "Failed to initialize stream_restore extension";
556 }
557 } else if (!PA_CONTEXT_IS_GOOD(state)) {
558 qCWarning(PULSEAUDIOQT) << "context kaput";
559 if (m_context) {
560 pa_context_unref(m_context);
561 m_context = nullptr;
562 }
563 if (!m_connectTimer.isActive() && hasConnectionTriesLeft()) {
564 reset();
565 qCDebug(PULSEAUDIOQT) << "Starting connect timer";
566 m_connectTimer.start(std::chrono::seconds(5));
567 Q_EMIT q->autoConnectingChanged();
568 }
569 }
570}
571
572void ContextPrivate::sinkCallback(const pa_sink_info *info)
573{
574 // This parenting here is a bit weird
575 m_sinks.updateEntry(info, q);
576}
577
578void ContextPrivate::sinkInputCallback(const pa_sink_input_info *info)
579{
580 m_sinkInputs.updateEntry(info, q);
581}
582
583void ContextPrivate::sourceCallback(const pa_source_info *info)
584{
585 m_sources.updateEntry(info, q);
586}
587
588void ContextPrivate::sourceOutputCallback(const pa_source_output_info *info)
589{
590 m_sourceOutputs.updateEntry(info, q);
591}
592
593void ContextPrivate::clientCallback(const pa_client_info *info)
594{
595 m_clients.updateEntry(info, q);
596}
597
598void ContextPrivate::cardCallback(const pa_card_info *info)
599{
600 m_cards.updateEntry(info, q);
601}
602
603void ContextPrivate::moduleCallback(const pa_module_info *info)
604{
605 m_modules.updateEntry(info, q);
606}
607
608void ContextPrivate::streamRestoreCallback(const pa_ext_stream_restore_info *info)
609{
610 if (qstrcmp(info->name, EVENT_ROLE) != 0) {
611 return;
612 }
613
614 const int eventRoleIndex = 0;
615 StreamRestore *obj = qobject_cast<StreamRestore *>(m_streamRestores.data().value(eventRoleIndex));
616
617 if (!obj) {
618 QVariantMap props;
619 props.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification"));
620 obj = new StreamRestore(eventRoleIndex, props, q);
621 obj->d->update(info);
622 m_streamRestores.insert(obj);
623 } else {
624 obj->d->update(info);
625 }
626}
627
628void ContextPrivate::serverCallback(const pa_server_info *info)
629{
630 m_server->d->update(info);
631}
632
633void Context::setCardProfile(quint32 index, const QString &profile)
634{
635 if (!d->m_context) {
636 return;
637 }
638 qCDebug(PULSEAUDIOQT) << index << profile;
639 if (!PAOperation(pa_context_set_card_profile_by_index(d->m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) {
640 qCWarning(PULSEAUDIOQT) << "pa_context_set_card_profile_by_index failed";
641 return;
642 }
643}
644
645void Context::setDefaultSink(const QString &name)
646{
647 if (!d->m_context) {
648 return;
649 }
650 const QByteArray nameData = name.toUtf8();
651 if (!PAOperation(pa_context_set_default_sink(d->m_context, nameData.constData(), nullptr, nullptr))) {
652 qCWarning(PULSEAUDIOQT) << "pa_context_set_default_sink failed";
653 }
654
655 // Change device for all entries in stream-restore database
656 d->m_newDefaultSink = name;
657 if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_sink_cb, d.get()))) {
658 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read failed";
659 }
660}
661
662void Context::setDefaultSource(const QString &name)
663{
664 if (!d->m_context) {
665 return;
666 }
667 const QByteArray nameData = name.toUtf8();
668 if (!PAOperation(pa_context_set_default_source(d->m_context, nameData.constData(), nullptr, nullptr))) {
669 qCWarning(PULSEAUDIOQT) << "pa_context_set_default_source failed";
670 }
671
672 // Change device for all entries in stream-restore database
673 d->m_newDefaultSource = name;
674 if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_source_cb, d.get()))) {
675 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read failed";
676 }
677}
678
679void ContextPrivate::streamRestoreWrite(const pa_ext_stream_restore_info *info)
680{
681 if (!m_context) {
682 return;
683 }
684 if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1, true, nullptr, nullptr))) {
685 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_write failed";
686 }
687}
688
689void ContextPrivate::connectToDaemon()
690{
691 if (m_context) {
692 return;
693 }
694
695 qCDebug(PULSEAUDIOQT) << "Connecting to daemon.";
696
697 m_state = Context::State::Connecting;
698 Q_EMIT q->stateChanged();
699
700 // We require a glib event loop
701 if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("Glib")) {
702 qCWarning(PULSEAUDIOQT) << "Disabling PulseAudio integration for lack of GLib event loop";
703 return;
704 }
705
706 qCDebug(PULSEAUDIOQT) << "Attempting connection to PulseAudio sound daemon";
707 if (!m_mainloop) {
708 m_mainloop = pa_glib_mainloop_new(nullptr);
709 Q_ASSERT(m_mainloop);
710 }
711
712 pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop);
713 Q_ASSERT(api);
714
715 pa_proplist *proplist = pa_proplist_new();
716 pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, QGuiApplication::applicationDisplayName().toUtf8().constData());
717 if (!s_applicationId.isEmpty()) {
718 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, s_applicationId.toUtf8().constData());
719 } else {
720 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, QGuiApplication::desktopFileName().toUtf8().constData());
721 }
722 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, QGuiApplication::windowIcon().name().toUtf8().constData());
723 m_context = pa_context_new_with_proplist(api, nullptr, proplist);
724 pa_proplist_free(proplist);
725 Q_ASSERT(m_context);
726
727 if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) {
728 qCWarning(PULSEAUDIOQT) << "Failed to connect context";
729 pa_context_unref(m_context);
730 pa_glib_mainloop_free(m_mainloop);
731 // Don't reset() here, it'd reset the retry count and possibly lead to infinite retries.
732 m_context = nullptr;
733 m_mainloop = nullptr;
734 m_state = Context::State::Unconnected;
735 Q_EMIT q->stateChanged();
736 return;
737 }
738 pa_context_set_state_callback(m_context, &context_state_callback, this);
739}
740
741void ContextPrivate::checkConnectTries()
742{
743 if (++m_connectTries; !hasConnectionTriesLeft()) {
744 qCWarning(PULSEAUDIOQT) << "Giving up after" << m_connectTries << "tries to connect";
745 m_connectTimer.stop();
746 Q_EMIT q->autoConnectingChanged();
747 }
748}
749
750void ContextPrivate::disconnectSignals()
751{
752 m_sinks.disconnectSignals();
753 m_sinkInputs.disconnectSignals();
754 m_sources.disconnectSignals();
755 m_sourceOutputs.disconnectSignals();
756 m_clients.disconnectSignals();
757 m_cards.disconnectSignals();
758 m_modules.disconnectSignals();
759 m_streamRestores.disconnectSignals();
760
761 m_server->disconnectSignals();
762
763 QObject::disconnect(q, &Context::stateChanged, nullptr, nullptr);
764 QObject::disconnect(q, &Context::autoConnectingChanged, nullptr, nullptr);
765}
766
767void ContextPrivate::reset()
768{
769 m_sinks.reset();
770 m_sinkInputs.reset();
771 m_sources.reset();
772 m_sourceOutputs.reset();
773 m_clients.reset();
774 m_cards.reset();
775 m_modules.reset();
776 m_streamRestores.reset();
777 m_server->reset();
778 m_connectTries = 0;
779 m_state = Context::State::Unconnected;
780 Q_EMIT q->stateChanged();
781}
782
783bool Context::isValid()
784{
785 return d->m_context && d->m_mainloop;
786}
787
788QList<Sink *> Context::sinks() const
789{
790 return d->m_sinks.data();
791}
792
793QList<SinkInput *> Context::sinkInputs() const
794{
795 return d->m_sinkInputs.data();
796}
797
798QList<Source *> Context::sources() const
799{
800 return d->m_sources.data();
801}
802
803QList<SourceOutput *> Context::sourceOutputs() const
804{
805 return d->m_sourceOutputs.data();
806}
807
808QList<Client *> Context::clients() const
809{
810 return d->m_clients.data();
811}
812
813QList<Card *> Context::cards() const
814{
815 return d->m_cards.data();
816}
817
818QList<Module *> Context::modules() const
819{
820 return d->m_modules.data();
821}
822
823QList<StreamRestore *> Context::streamRestores() const
824{
825 return d->m_streamRestores.data();
826}
827
828Server *Context::server() const
829{
830 return d->m_server;
831}
832
833void ContextPrivate::setGenericVolume(
834 quint32 index,
835 int channel,
836 qint64 newVolume,
837 pa_cvolume cVolume,
838 const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
839{
840 if (!m_context) {
841 return;
842 }
843 newVolume = qBound<qint64>(0, newVolume, PA_VOLUME_MAX);
844 pa_cvolume newCVolume = cVolume;
845 if (channel == -1) { // -1 all channels
846 const qint64 orig = pa_cvolume_max(&cVolume);
847 const qint64 diff = newVolume - orig;
848 for (int i = 0; i < newCVolume.channels; ++i) {
849 const qint64 channel = newCVolume.values[i];
850 const qint64 channelDiff = orig == 0 ? diff : diff * channel / orig;
851 newCVolume.values[i] = qBound<qint64>(0, newCVolume.values[i] + channelDiff, PA_VOLUME_MAX);
852 }
853 } else {
854 Q_ASSERT(newCVolume.channels > channel);
855 newCVolume.values[channel] = newVolume;
856 }
857 if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
858 qCWarning(PULSEAUDIOQT) << "pa_set_volume failed";
859 return;
860 }
861}
862
863void ContextPrivate::setGenericMute(quint32 index,
864 bool mute,
865 const std::function<pa_operation *(pa_context *, uint32_t, int, pa_context_success_cb_t, void *)> &pa_set_mute)
866{
867 if (!m_context) {
868 return;
869 }
870 if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) {
871 qCWarning(PULSEAUDIOQT) << "pa_set_mute failed";
872 return;
873 }
874}
875
876void ContextPrivate::setGenericPort(quint32 index,
877 const QString &portName,
878 const std::function<pa_operation *(pa_context *, uint32_t, const char *, pa_context_success_cb_t, void *)> &pa_set_port)
879{
880 if (!m_context) {
881 return;
882 }
883 if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) {
884 qCWarning(PULSEAUDIOQT) << "pa_set_port failed";
885 return;
886 }
887}
888
889void ContextPrivate::setGenericDeviceForStream(
890 quint32 streamIndex,
891 quint32 deviceIndex,
892 const std::function<pa_operation *(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *)> &pa_move_stream_to_device)
893{
894 if (!m_context) {
895 return;
896 }
897 if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) {
898 qCWarning(PULSEAUDIOQT) << "pa_move_stream_to_device failed";
899 return;
900 }
901}
902
903void ContextPrivate::setGenericVolumes(
904 quint32 index,
905 QList<qint64> channelVolumes,
906 pa_cvolume cVolume,
907 const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
908{
909 if (!m_context) {
910 return;
911 }
912 Q_ASSERT(channelVolumes.count() == cVolume.channels);
913
914 pa_cvolume newCVolume = cVolume;
915 for (int i = 0; i < channelVolumes.count(); ++i) {
916 newCVolume.values[i] = qBound<qint64>(0, channelVolumes.at(i), PA_VOLUME_MAX);
917 }
918
919 if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
920 qCWarning(PULSEAUDIOQT) << "pa_set_volume failed";
921 return;
922 }
923}
924
925void Context::setApplicationId(const QString &applicationId)
926{
927 ContextPrivate::s_applicationId = applicationId;
928}
929
930pa_context *Context::context() const
931{
932 return d->m_context;
933}
934
935Context::State Context::state() const
936{
937 return d->m_state;
938}
939
940bool Context::isAutoConnecting() const
941{
942 return d->m_connectTimer.isActive();
943}
944
945void Context::reconnectDaemon()
946{
947 if (isAutoConnecting()) { // must not be in the dptr; the dptr function is called by the auto connecting logic
948 qCDebug(PULSEAUDIOQT) << "Already in the process of auto connecting. Not connecting again.";
949 return;
950 }
951
952 d->forceDisconnect();
953 return d->connectToDaemon();
954}
955
956void ContextPrivate::forceDisconnect()
957{
958 if (m_context) {
959 pa_context_unref(m_context);
960 m_context = nullptr;
961 }
962
963 if (m_mainloop) {
964 pa_glib_mainloop_free(m_mainloop);
965 m_mainloop = nullptr;
966 }
967}
968
969bool ContextPrivate::hasConnectionTriesLeft() const
970{
971 constexpr auto maxTries = 5;
972 return m_connectTries < maxTries;
973}
974
975void ContextPrivate::synthesizeEventStream()
976{
977 const pa_ext_stream_restore_info info{
978 .name = EVENT_ROLE, //
979 .channel_map = {.channels = 1, .map = {PA_CHANNEL_POSITION_MONO}}, //
980 .volume = {.channels = 1, .values = {PA_VOLUME_NORM}}, //
981 .device = nullptr,
982 .mute = false,
983 };
984 streamRestoreCallback(&info);
985}
986
987void Context::loadModule(const QString &name, const QString &argument)
988{
989 // We could provide callback forwarding here, unfortunately that is a bit complicated because we don't do this elsewhere, so we'd have to invent
990 // new machinery. Additionally complicated by the fact that the caller may wish to supply a capturing lambda which doesn't decay to a pointer callable.
991 // Let's not do this until someone presents a good use case. Worst case, a caller can just use the context directly and side step this function.
992 if (!PAOperation(pa_context_load_module(d->m_context, qUtf8Printable(name), qUtf8Printable(argument), /*callback*/ nullptr, /*userdata*/ nullptr))) {
993 qCWarning(PULSEAUDIOQT) << "pa_context_load_module() failed" << name << argument;
994 }
995}
996
997void Context::unloadModule(PulseAudioQt::Module *module)
998{
999 if (!PAOperation(pa_context_unload_module(d->m_context, module->index(), /*callback*/ nullptr, /*userdata*/ nullptr))) {
1000 qCWarning(PULSEAUDIOQT) << "pa_context_load_module() failed" << module->index() << module->name() << module->argument();
1001 }
1002}
1003} // PulseAudioQt
QString name(StandardAction id)
KGuiItem reset()
The primary namespace of PulseAudioQt.
Definition card.cpp:17
qint64 normalVolume()
The normal volume (100%, 0 dB).
Definition context.cpp:35
qint64 maximumUIVolume()
The maximum volume suitable to display in a UI.
Definition context.cpp:50
qint64 minimumVolume()
The minimum volume (0%).
Definition context.cpp:40
qint64 maximumVolume()
The maximum volume PulseAudio can store.
Definition context.cpp:45
QAbstractEventDispatcher * instance(QThread *thread)
const char * constData() const const
QDBusConnection sessionBus()
void serviceRegistered(const QString &serviceName)
const_reference at(qsizetype i) const const
qsizetype count() const const
pointer data()
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
bool disconnect(const QMetaObject::Connection &connection)
QByteArray toUtf8() const const
QueuedConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:57:59 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.