11 #include <QAbstractEventDispatcher>
12 #include <QDBusConnection>
13 #include <QDBusServiceWatcher>
14 #include <QGuiApplication>
24 #include "sinkinput.h"
26 #include "sourceoutput.h"
27 #include "streamrestore.h"
29 #include "context_p.h"
31 #include "streamrestore_p.h"
37 return PA_VOLUME_NORM;
42 return PA_VOLUME_MUTED;
52 return PA_VOLUME_UI_MAX;
55 QString ContextPrivate::s_applicationId;
59 static bool isGoodState(
int eol)
76 static void sink_cb(pa_context *context,
const pa_sink_info *info,
int eol,
void *data)
78 if (!isGoodState(eol))
82 static_cast<ContextPrivate *
>(data)->sinkCallback(info);
85 static void sink_input_callback(pa_context *context,
const pa_sink_input_info *info,
int eol,
void *data)
87 if (!isGoodState(eol))
90 if (qstrcmp(info->name,
"pulsesink probe") == 0) {
93 if (
const char *
id = pa_proplist_gets(info->proplist,
"module-stream-restore.id")) {
94 if (qstrcmp(
id,
"sink-input-by-media-role:event") == 0) {
95 qCDebug(PULSEAUDIOQT) <<
"Ignoring event role sink input.";
101 static_cast<ContextPrivate *
>(data)->sinkInputCallback(info);
104 static void source_cb(pa_context *context,
const pa_source_info *info,
int eol,
void *data)
106 if (!isGoodState(eol))
109 if (info->monitor_of_sink != PA_INVALID_INDEX)
113 static_cast<ContextPrivate *
>(data)->sourceCallback(info);
116 static void source_output_cb(pa_context *context,
const pa_source_output_info *info,
int eol,
void *data)
118 if (!isGoodState(eol))
121 if (
const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) {
122 if (strcmp(app,
"org.PulseAudio.pavucontrol") == 0
123 || strcmp(app,
"org.gnome.VolumeControl") == 0
124 || strcmp(app,
"org.kde.kmixd") == 0
125 || strcmp(app,
"org.kde.plasma-pa") == 0)
130 static_cast<ContextPrivate *
>(data)->sourceOutputCallback(info);
133 static void client_cb(pa_context *context,
const pa_client_info *info,
int eol,
void *data)
135 if (!isGoodState(eol))
139 static_cast<ContextPrivate *
>(data)->clientCallback(info);
142 static void card_cb(pa_context *context,
const pa_card_info *info,
int eol,
void *data)
144 if (!isGoodState(eol))
148 static_cast<ContextPrivate *
>(data)->cardCallback(info);
151 static void module_info_list_cb(pa_context *context,
const pa_module_info *info,
int eol,
void *data)
153 if (!isGoodState(eol))
157 static_cast<ContextPrivate *
>(data)->moduleCallback(info);
160 static void server_cb(pa_context *context,
const pa_server_info *info,
void *data)
167 qCWarning(PULSEAUDIOQT) <<
"server_cb() called without info!";
170 static_cast<ContextPrivate *
>(data)->serverCallback(info);
173 static void context_state_callback(pa_context *context,
void *data)
176 static_cast<ContextPrivate *
>(data)->contextStateCallback(context);
179 static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index,
void *data)
182 static_cast<ContextPrivate *
>(data)->subscribeCallback(context, type, index);
185 static void ext_stream_restore_read_cb(pa_context *context,
const pa_ext_stream_restore_info *info,
int eol,
void *data)
187 if (!isGoodState(eol)) {
192 static_cast<ContextPrivate *
>(data)->streamRestoreCallback(info);
195 static void ext_stream_restore_subscribe_cb(pa_context *context,
void *data)
199 if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) {
200 qCWarning(PULSEAUDIOQT) <<
"pa_ext_stream_restore_read() failed";
204 static void ext_stream_restore_change_sink_cb(pa_context *context,
const pa_ext_stream_restore_info *info,
int eol,
void *data)
206 if (!isGoodState(eol)) {
211 if (qstrncmp(info->name,
"sink-input-by", 13) == 0) {
212 ContextPrivate *contextp =
static_cast<ContextPrivate *
>(data);
213 const QByteArray deviceData = contextp->m_newDefaultSink.toUtf8();
214 pa_ext_stream_restore_info newinfo;
215 newinfo.name = info->name;
216 newinfo.channel_map = info->channel_map;
217 newinfo.volume = info->volume;
218 newinfo.mute = info->mute;
220 contextp->streamRestoreWrite(&newinfo);
224 static void ext_stream_restore_change_source_cb(pa_context *context,
const pa_ext_stream_restore_info *info,
int eol,
void *data)
226 if (!isGoodState(eol)) {
231 if (qstrncmp(info->name,
"source-output-by", 16) == 0) {
232 ContextPrivate *contextp =
static_cast<ContextPrivate *
>(data);
233 const QByteArray deviceData = contextp->m_newDefaultSource.toUtf8();
234 pa_ext_stream_restore_info newinfo;
235 newinfo.name = info->name;
236 newinfo.channel_map = info->channel_map;
237 newinfo.volume = info->volume;
238 newinfo.mute = info->mute;
240 contextp->streamRestoreWrite(&newinfo);
248 Context::Context(
QObject *parent)
250 , d(new ContextPrivate(this))
252 d->m_server =
new Server(
this);
253 d->m_context =
nullptr;
254 d->m_mainloop =
nullptr;
257 d->connectToDaemon();
262 d->connectToDaemon();
265 connect(&d->m_sinks, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
266 Q_EMIT sinkAdded(static_cast<Sink *>(object));
268 connect(&d->m_sinks, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
269 Q_EMIT sinkRemoved(static_cast<Sink *>(object));
272 connect(&d->m_sinkInputs, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
273 Q_EMIT sinkInputAdded(static_cast<SinkInput *>(object));
275 connect(&d->m_sinkInputs, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
276 Q_EMIT sinkInputRemoved(static_cast<SinkInput *>(object));
279 connect(&d->m_sources, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
280 Q_EMIT sourceAdded(static_cast<Source *>(object));
282 connect(&d->m_sources, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
283 Q_EMIT sourceRemoved(static_cast<Source *>(object));
286 connect(&d->m_sourceOutputs, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
287 Q_EMIT sourceOutputAdded(static_cast<SourceOutput *>(object));
289 connect(&d->m_sourceOutputs, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
290 Q_EMIT sourceOutputRemoved(static_cast<SourceOutput *>(object));
293 connect(&d->m_clients, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
294 Q_EMIT clientAdded(static_cast<Client *>(object));
296 connect(&d->m_clients, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
297 Q_EMIT clientRemoved(static_cast<Client *>(object));
300 connect(&d->m_cards, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
301 Q_EMIT cardAdded(static_cast<Card *>(object));
303 connect(&d->m_cards, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
304 Q_EMIT cardRemoved(static_cast<Card *>(object));
307 connect(&d->m_modules, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
308 Q_EMIT moduleAdded(static_cast<Module *>(object));
310 connect(&d->m_modules, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
311 Q_EMIT moduleRemoved(static_cast<Module *>(object));
314 connect(&d->m_streamRestores, &MapBaseQObject::added,
this, [
this](
int,
QObject *
object) {
315 Q_EMIT streamRestoreAdded(static_cast<StreamRestore *>(object));
317 connect(&d->m_streamRestores, &MapBaseQObject::removed,
this, [
this](
int,
QObject *
object) {
318 Q_EMIT streamRestoreRemoved(static_cast<StreamRestore *>(object));
322 ContextPrivate::ContextPrivate(Context *q)
332 ContextPrivate::~ContextPrivate()
335 pa_context_unref(m_context);
340 pa_glib_mainloop_free(m_mainloop);
341 m_mainloop =
nullptr;
347 Context *Context::instance()
349 static std::unique_ptr<Context> context(
new Context);
350 return context.get();
353 void ContextPrivate::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index)
355 Q_ASSERT(context == m_context);
357 switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
358 case PA_SUBSCRIPTION_EVENT_SINK:
359 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
360 m_sinks.removeEntry(index);
362 if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb,
this))) {
363 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_sink_info_by_index() failed";
369 case PA_SUBSCRIPTION_EVENT_SOURCE:
370 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
371 m_sources.removeEntry(index);
373 if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb,
this))) {
374 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_source_info_by_index() failed";
380 case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
381 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
382 m_sinkInputs.removeEntry(index);
384 if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback,
this))) {
385 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_sink_input_info() failed";
391 case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
392 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
393 m_sourceOutputs.removeEntry(index);
395 if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb,
this))) {
396 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_sink_input_info() failed";
402 case PA_SUBSCRIPTION_EVENT_CLIENT:
403 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
404 m_clients.removeEntry(index);
406 if (!PAOperation(pa_context_get_client_info(context, index, client_cb,
this))) {
407 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_client_info() failed";
413 case PA_SUBSCRIPTION_EVENT_CARD:
414 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
415 m_cards.removeEntry(index);
417 if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb,
this))) {
418 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_card_info_by_index() failed";
424 case PA_SUBSCRIPTION_EVENT_MODULE:
425 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
426 m_modules.removeEntry(index);
428 if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb,
this))) {
429 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_module_info_list() failed";
435 case PA_SUBSCRIPTION_EVENT_SERVER:
436 if (!PAOperation(pa_context_get_server_info(context, server_cb,
this))) {
437 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_server_info() failed";
444 void ContextPrivate::contextStateCallback(pa_context *c)
446 qCDebug(PULSEAUDIOQT) <<
"state callback";
447 pa_context_state_t state = pa_context_get_state(c);
448 if (state == PA_CONTEXT_READY) {
449 qCDebug(PULSEAUDIOQT) <<
"ready";
452 if (m_context == c) {
453 pa_context_set_subscribe_callback(c, subscribe_cb,
this);
456 pa_context_subscribe(c,
457 (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_CLIENT
458 | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT
459 | PA_SUBSCRIPTION_MASK_CARD | PA_SUBSCRIPTION_MASK_MODULE | PA_SUBSCRIPTION_MASK_SERVER),
462 qCWarning(PULSEAUDIOQT) <<
"pa_context_subscribe() failed";
467 if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb,
this))) {
468 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_sink_info_list() failed";
472 if (!PAOperation(pa_context_get_source_info_list(c, source_cb,
this))) {
473 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_source_info_list() failed";
477 if (!PAOperation(pa_context_get_client_info_list(c, client_cb,
this))) {
478 qCWarning(PULSEAUDIOQT) <<
"pa_context_client_info_list() failed";
482 if (!PAOperation(pa_context_get_card_info_list(c, card_cb,
this))) {
483 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_card_info_list() failed";
487 if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback,
this))) {
488 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_sink_input_info_list() failed";
492 if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb,
this))) {
493 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_source_output_info_list() failed";
497 if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb,
this))) {
498 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_module_info_list() failed";
502 if (!PAOperation(pa_context_get_server_info(c, server_cb,
this))) {
503 qCWarning(PULSEAUDIOQT) <<
"pa_context_get_server_info() failed";
507 if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb,
this))) {
508 pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb,
this);
509 PAOperation(pa_ext_stream_restore_subscribe(c, 1,
nullptr,
this));
511 qCWarning(PULSEAUDIOQT) <<
"Failed to initialize stream_restore extension";
513 }
else if (!PA_CONTEXT_IS_GOOD(state)) {
514 qCWarning(PULSEAUDIOQT) <<
"context kaput";
516 pa_context_unref(m_context);
526 void ContextPrivate::sinkCallback(
const pa_sink_info *info)
529 m_sinks.updateEntry(info, q);
532 void ContextPrivate::sinkInputCallback(
const pa_sink_input_info *info)
534 m_sinkInputs.updateEntry(info, q);
537 void ContextPrivate::sourceCallback(
const pa_source_info *info)
539 m_sources.updateEntry(info, q);
542 void ContextPrivate::sourceOutputCallback(
const pa_source_output_info *info)
544 m_sourceOutputs.updateEntry(info, q);
547 void ContextPrivate::clientCallback(
const pa_client_info *info)
549 m_clients.updateEntry(info, q);
552 void ContextPrivate::cardCallback(
const pa_card_info *info)
554 m_cards.updateEntry(info, q);
557 void ContextPrivate::moduleCallback(
const pa_module_info *info)
559 m_modules.updateEntry(info, q);
562 void ContextPrivate::streamRestoreCallback(
const pa_ext_stream_restore_info *info)
564 if (qstrcmp(info->name,
"sink-input-by-media-role:event") != 0) {
568 const int eventRoleIndex = 1;
569 StreamRestore *obj = qobject_cast<StreamRestore *>(m_streamRestores.data().value(eventRoleIndex));
573 props.insert(QStringLiteral(
"application.icon_name"), QStringLiteral(
"preferences-desktop-notification"));
574 obj =
new StreamRestore(eventRoleIndex, props, q);
575 obj->d->update(info);
576 m_streamRestores.insert(obj);
578 obj->d->update(info);
582 void ContextPrivate::serverCallback(
const pa_server_info *info)
584 m_server->d->update(info);
587 void Context::setCardProfile(quint32 index,
const QString &profile)
592 qCDebug(PULSEAUDIOQT) << index << profile;
593 if (!PAOperation(pa_context_set_card_profile_by_index(d->m_context, index, profile.toUtf8().constData(),
nullptr,
nullptr))) {
594 qCWarning(PULSEAUDIOQT) <<
"pa_context_set_card_profile_by_index failed";
599 void Context::setDefaultSink(
const QString &name)
605 if (!PAOperation(pa_context_set_default_sink(d->m_context, nameData.
constData(),
nullptr,
nullptr))) {
606 qCWarning(PULSEAUDIOQT) <<
"pa_context_set_default_sink failed";
610 d->m_newDefaultSink =
name;
611 if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_sink_cb, d))) {
612 qCWarning(PULSEAUDIOQT) <<
"pa_ext_stream_restore_read failed";
616 void Context::setDefaultSource(
const QString &name)
622 if (!PAOperation(pa_context_set_default_source(d->m_context, nameData.
constData(),
nullptr,
nullptr))) {
623 qCWarning(PULSEAUDIOQT) <<
"pa_context_set_default_source failed";
627 d->m_newDefaultSource =
name;
628 if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_source_cb, d))) {
629 qCWarning(PULSEAUDIOQT) <<
"pa_ext_stream_restore_read failed";
633 void ContextPrivate::streamRestoreWrite(
const pa_ext_stream_restore_info *info)
638 if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1,
true,
nullptr,
nullptr))) {
639 qCWarning(PULSEAUDIOQT) <<
"pa_ext_stream_restore_write failed";
643 void ContextPrivate::connectToDaemon()
651 qCWarning(PULSEAUDIOQT) <<
"Disabling PulseAudio integration for lack of GLib event loop";
655 qCDebug(PULSEAUDIOQT) <<
"Attempting connection to PulseAudio sound daemon";
657 m_mainloop = pa_glib_mainloop_new(
nullptr);
658 Q_ASSERT(m_mainloop);
661 pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop);
664 pa_proplist *proplist = pa_proplist_new();
666 if (!s_applicationId.isEmpty()) {
667 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, s_applicationId.toUtf8().constData());
672 m_context = pa_context_new_with_proplist(api,
nullptr, proplist);
673 pa_proplist_free(proplist);
676 if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL,
nullptr) < 0) {
677 pa_context_unref(m_context);
678 pa_glib_mainloop_free(m_mainloop);
680 m_mainloop =
nullptr;
683 pa_context_set_state_callback(m_context, &context_state_callback,
this);
686 void ContextPrivate::reset()
689 m_sinkInputs.reset();
691 m_sourceOutputs.reset();
695 m_streamRestores.reset();
699 bool Context::isValid()
701 return d->m_context && d->m_mainloop;
706 return d->m_sinks.
data();
711 return d->m_sinkInputs.
data();
716 return d->m_sources.
data();
721 return d->m_sourceOutputs.
data();
726 return d->m_clients.
data();
731 return d->m_cards.
data();
736 return d->m_modules.
data();
741 return d->m_streamRestores.
data();
744 Server *Context::server()
const
749 void ContextPrivate::setGenericVolume(
754 const std::function<pa_operation *(pa_context *, uint32_t,
const pa_cvolume *, pa_context_success_cb_t,
void *)> &pa_set_volume)
759 newVolume = qBound<qint64>(0, newVolume, PA_VOLUME_MAX);
760 pa_cvolume newCVolume = cVolume;
762 const qint64 diff = newVolume - pa_cvolume_max(&cVolume);
763 for (
int i = 0; i < newCVolume.channels; ++i) {
764 newCVolume.values[i] = qBound<qint64>(0, newCVolume.values[i] + diff, PA_VOLUME_MAX);
767 Q_ASSERT(newCVolume.channels > channel);
768 newCVolume.values[channel] = newVolume;
770 if (!pa_set_volume(m_context, index, &newCVolume,
nullptr,
nullptr)) {
771 qCWarning(PULSEAUDIOQT) <<
"pa_set_volume failed";
776 void ContextPrivate::setGenericMute(quint32 index,
778 const std::function<pa_operation *(pa_context *, uint32_t,
int, pa_context_success_cb_t,
void *)> &pa_set_mute)
783 if (!PAOperation(pa_set_mute(m_context, index, mute,
nullptr,
nullptr))) {
784 qCWarning(PULSEAUDIOQT) <<
"pa_set_mute failed";
789 void ContextPrivate::setGenericPort(quint32 index,
791 const std::function<pa_operation *(pa_context *, uint32_t,
const char *, pa_context_success_cb_t,
void *)> &pa_set_port)
796 if (!PAOperation(pa_set_port(m_context, index, portName.
toUtf8().
constData(),
nullptr,
nullptr))) {
797 qCWarning(PULSEAUDIOQT) <<
"pa_set_port failed";
802 void ContextPrivate::setGenericDeviceForStream(
805 const std::function<pa_operation *(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t,
void *)> &pa_move_stream_to_device)
810 if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex,
nullptr,
nullptr))) {
811 qCWarning(PULSEAUDIOQT) <<
"pa_move_stream_to_device failed";
816 void ContextPrivate::setGenericVolumes(
820 const std::function<pa_operation *(pa_context *, uint32_t,
const pa_cvolume *, pa_context_success_cb_t,
void *)> &pa_set_volume)
825 Q_ASSERT(channelVolumes.
count() == cVolume.channels);
827 pa_cvolume newCVolume = cVolume;
828 for (
int i = 0; i < channelVolumes.
count(); ++i) {
829 newCVolume.values[i] = qBound<qint64>(0, channelVolumes.
at(i), PA_VOLUME_MAX);
832 if (!PAOperation(pa_set_volume(m_context, index, &newCVolume,
nullptr,
nullptr))) {
833 qCWarning(PULSEAUDIOQT) <<
"pa_set_volume failed";
838 void Context::setApplicationId(
const QString &applicationId)
840 ContextPrivate::s_applicationId = applicationId;
843 pa_context *Context::context()
const