/* SPDX-FileCopyrightText: 2014-2015 Harald Sitter SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include "context.h" #include "server.h" #include "debug.h" #include #include #include #include #include #include #include #include "card.h" #include "client.h" #include "module.h" #include "sink.h" #include "sinkinput.h" #include "source.h" #include "sourceoutput.h" #include "streamrestore.h" #include "context_p.h" #include "server_p.h" #include "streamrestore_p.h" namespace PulseAudioQt { qint64 normalVolume() { return PA_VOLUME_NORM; } qint64 minimumVolume() { return PA_VOLUME_MUTED; } qint64 maximumVolume() { return PA_VOLUME_MAX; } qint64 maximumUIVolume() { return PA_VOLUME_UI_MAX; } QString ContextPrivate::s_applicationId; #ifndef K_DOXYGEN static bool isGoodState(int eol) { if (eol < 0) { // Error return false; } if (eol > 0) { // End of callback chain return false; } return true; } // -------------------------- static void sink_cb(pa_context *context, const pa_sink_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->sinkCallback(info); } static void sink_input_callback(pa_context *context, const pa_sink_input_info *info, int eol, void *data) { if (!isGoodState(eol)) return; // pulsesink probe is used by gst-pulse only to query sink formats (not for playback) if (qstrcmp(info->name, "pulsesink probe") == 0) { return; } if (const char *id = pa_proplist_gets(info->proplist, "module-stream-restore.id")) { if (qstrcmp(id, "sink-input-by-media-role:event") == 0) { qDebug() << "Ignoring event role sink input."; return; } } Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->sinkInputCallback(info); } static void source_cb(pa_context *context, const pa_source_info *info, int eol, void *data) { if (!isGoodState(eol)) return; // FIXME: This forces excluding monitors if (info->monitor_of_sink != PA_INVALID_INDEX) return; Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->sourceCallback(info); } static void source_output_cb(pa_context *context, const pa_source_output_info *info, int eol, void *data) { if (!isGoodState(eol)) return; // FIXME: This forces excluding these apps if (const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) { if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 // || strcmp(app, "org.gnome.VolumeControl") == 0 // || strcmp(app, "org.kde.kmixd") == 0 // || strcmp(app, "org.kde.plasma-pa") == 0) // return; } Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->sourceOutputCallback(info); } static void client_cb(pa_context *context, const pa_client_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->clientCallback(info); } static void card_cb(pa_context *context, const pa_card_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->cardCallback(info); } static void module_info_list_cb(pa_context *context, const pa_module_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->moduleCallback(info); } static void server_cb(pa_context *context, const pa_server_info *info, void *data) { Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->serverCallback(info); } static void context_state_callback(pa_context *context, void *data) { Q_ASSERT(data); static_cast(data)->contextStateCallback(context); } static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data) { Q_ASSERT(data); static_cast(data)->subscribeCallback(context, type, index); } static void ext_stream_restore_read_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) { if (!isGoodState(eol)) { return; } Q_ASSERT(context); Q_ASSERT(data); static_cast(data)->streamRestoreCallback(info); } static void ext_stream_restore_subscribe_cb(pa_context *context, void *data) { Q_ASSERT(context); Q_ASSERT(data); if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) { qWarning() << "pa_ext_stream_restore_read() failed"; } } static void ext_stream_restore_change_sink_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) { if (!isGoodState(eol)) { return; } Q_ASSERT(context); Q_ASSERT(data); if (qstrncmp(info->name, "sink-input-by", 13) == 0) { ContextPrivate *contextp = static_cast(data); const QByteArray deviceData = contextp->m_newDefaultSink.toUtf8(); pa_ext_stream_restore_info newinfo; newinfo.name = info->name; newinfo.channel_map = info->channel_map; newinfo.volume = info->volume; newinfo.mute = info->mute; newinfo.device = deviceData.constData(); contextp->streamRestoreWrite(&newinfo); } } static void ext_stream_restore_change_source_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) { if (!isGoodState(eol)) { return; } Q_ASSERT(context); Q_ASSERT(data); if (qstrncmp(info->name, "source-output-by", 16) == 0) { ContextPrivate *contextp = static_cast(data); const QByteArray deviceData = contextp->m_newDefaultSource.toUtf8(); pa_ext_stream_restore_info newinfo; newinfo.name = info->name; newinfo.channel_map = info->channel_map; newinfo.volume = info->volume; newinfo.mute = info->mute; newinfo.device = deviceData.constData(); contextp->streamRestoreWrite(&newinfo); } } #endif // -------------------------- Context::Context(QObject *parent) : QObject(parent) , d(new ContextPrivate(this)) { d->m_server = new Server(this); d->m_context = nullptr; d->m_mainloop = nullptr; d->m_references = 0; d->connectToDaemon(); QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this); connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] { d->connectToDaemon(); }); connect(&d->m_sinks, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT sinkAdded(static_cast(object)); }); connect(&d->m_sinks, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT sinkRemoved(static_cast(object)); }); connect(&d->m_sinkInputs, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT sinkInputAdded(static_cast(object)); }); connect(&d->m_sinkInputs, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT sinkInputRemoved(static_cast(object)); }); connect(&d->m_sources, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT sourceAdded(static_cast(object)); }); connect(&d->m_sources, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT sourceRemoved(static_cast(object)); }); connect(&d->m_sourceOutputs, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT sourceOutputAdded(static_cast(object)); }); connect(&d->m_sourceOutputs, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT sourceOutputRemoved(static_cast(object)); }); connect(&d->m_clients, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT clientAdded(static_cast(object)); }); connect(&d->m_clients, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT clientRemoved(static_cast(object)); }); connect(&d->m_cards, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT cardAdded(static_cast(object)); }); connect(&d->m_cards, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT cardRemoved(static_cast(object)); }); connect(&d->m_modules, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT moduleAdded(static_cast(object)); }); connect(&d->m_modules, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT moduleRemoved(static_cast(object)); }); connect(&d->m_streamRestores, &MapBaseQObject::added, this, [this](int, QObject *object) { Q_EMIT streamRestoreAdded(static_cast(object)); }); connect(&d->m_streamRestores, &MapBaseQObject::removed, this, [this](int, QObject *object) { Q_EMIT streamRestoreRemoved(static_cast(object)); }); } ContextPrivate::ContextPrivate(Context *q) : q(q) { } Context::~Context() { delete d; } ContextPrivate::~ContextPrivate() { if (m_context) { pa_context_unref(m_context); m_context = nullptr; } if (m_mainloop) { pa_glib_mainloop_free(m_mainloop); m_mainloop = nullptr; } reset(); } Context *Context::instance() { static std::unique_ptr context(new Context); return context.get(); } void ContextPrivate::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index) { Q_ASSERT(context == m_context); switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { case PA_SUBSCRIPTION_EVENT_SINK: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sinks.removeEntry(index); } else { if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) { qWarning() << "pa_context_get_sink_info_by_index() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SOURCE: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sources.removeEntry(index); } else { if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) { qWarning() << "pa_context_get_source_info_by_index() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SINK_INPUT: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sinkInputs.removeEntry(index); } else { if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) { qWarning() << "pa_context_get_sink_input_info() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sourceOutputs.removeEntry(index); } else { if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) { qWarning() << "pa_context_get_sink_input_info() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_CLIENT: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_clients.removeEntry(index); } else { if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) { qWarning() << "pa_context_get_client_info() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_CARD: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_cards.removeEntry(index); } else { if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) { qWarning() << "pa_context_get_card_info_by_index() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_MODULE: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_modules.removeEntry(index); } else { if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) { qWarning() << "pa_context_get_module_info_list() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SERVER: if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) { qWarning() << "pa_context_get_server_info() failed"; return; } break; } } void ContextPrivate::contextStateCallback(pa_context *c) { qDebug() << "state callback"; pa_context_state_t state = pa_context_get_state(c); if (state == PA_CONTEXT_READY) { qDebug() << "ready"; // 1. Register for the stream changes (except during probe) if (m_context == c) { pa_context_set_subscribe_callback(c, subscribe_cb, this); if (!PAOperation( pa_context_subscribe(c, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_CLIENT | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT | PA_SUBSCRIPTION_MASK_CARD | PA_SUBSCRIPTION_MASK_MODULE | PA_SUBSCRIPTION_MASK_SERVER), nullptr, nullptr))) { qWarning() << "pa_context_subscribe() failed"; return; } } if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) { qWarning() << "pa_context_get_sink_info_list() failed"; return; } if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) { qWarning() << "pa_context_get_source_info_list() failed"; return; } if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) { qWarning() << "pa_context_client_info_list() failed"; return; } if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) { qWarning() << "pa_context_get_card_info_list() failed"; return; } if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) { qWarning() << "pa_context_get_sink_input_info_list() failed"; return; } if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) { qWarning() << "pa_context_get_source_output_info_list() failed"; return; } if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) { qWarning() << "pa_context_get_module_info_list() failed"; return; } if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) { qWarning() << "pa_context_get_server_info() failed"; return; } if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) { pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this); PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this)); } else { qWarning() << "Failed to initialize stream_restore extension"; } } else if (!PA_CONTEXT_IS_GOOD(state)) { qWarning() << "context kaput"; if (m_context) { pa_context_unref(m_context); m_context = nullptr; } reset(); QTimer::singleShot(1000, q, [this] { connectToDaemon(); }); } } void ContextPrivate::sinkCallback(const pa_sink_info *info) { // This parenting here is a bit weird m_sinks.updateEntry(info, q); } void ContextPrivate::sinkInputCallback(const pa_sink_input_info *info) { m_sinkInputs.updateEntry(info, q); } void ContextPrivate::sourceCallback(const pa_source_info *info) { m_sources.updateEntry(info, q); } void ContextPrivate::sourceOutputCallback(const pa_source_output_info *info) { m_sourceOutputs.updateEntry(info, q); } void ContextPrivate::clientCallback(const pa_client_info *info) { m_clients.updateEntry(info, q); } void ContextPrivate::cardCallback(const pa_card_info *info) { m_cards.updateEntry(info, q); } void ContextPrivate::moduleCallback(const pa_module_info *info) { m_modules.updateEntry(info, q); } void ContextPrivate::streamRestoreCallback(const pa_ext_stream_restore_info *info) { if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) { return; } const int eventRoleIndex = 1; StreamRestore *obj = qobject_cast(m_streamRestores.data().value(eventRoleIndex)); if (!obj) { QVariantMap props; props.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification")); obj = new StreamRestore(eventRoleIndex, props, q); obj->d->update(info); m_streamRestores.insert(obj); } else { obj->d->update(info); } } void ContextPrivate::serverCallback(const pa_server_info *info) { m_server->d->update(info); } void Context::setCardProfile(quint32 index, const QString &profile) { if (!d->m_context) { return; } qDebug() << index << profile; if (!PAOperation(pa_context_set_card_profile_by_index(d->m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) { qWarning() << "pa_context_set_card_profile_by_index failed"; return; } } void Context::setDefaultSink(const QString &name) { if (!d->m_context) { return; } const QByteArray nameData = name.toUtf8(); if (!PAOperation(pa_context_set_default_sink(d->m_context, nameData.constData(), nullptr, nullptr))) { qWarning() << "pa_context_set_default_sink failed"; } // Change device for all entries in stream-restore database d->m_newDefaultSink = name; if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_sink_cb, d))) { qWarning() << "pa_ext_stream_restore_read failed"; } } void Context::setDefaultSource(const QString &name) { if (!d->m_context) { return; } const QByteArray nameData = name.toUtf8(); if (!PAOperation(pa_context_set_default_source(d->m_context, nameData.constData(), nullptr, nullptr))) { qWarning() << "pa_context_set_default_source failed"; } // Change device for all entries in stream-restore database d->m_newDefaultSource = name; if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_source_cb, d))) { qWarning() << "pa_ext_stream_restore_read failed"; } } void ContextPrivate::streamRestoreWrite(const pa_ext_stream_restore_info *info) { if (!m_context) { return; } if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1, true, nullptr, nullptr))) { qWarning() << "pa_ext_stream_restore_write failed"; } } void ContextPrivate::connectToDaemon() { if (m_context) { return; } // We require a glib event loop if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("Glib")) { qWarning() << "Disabling PulseAudio integration for lack of GLib event loop"; return; } qDebug() << "Attempting connection to PulseAudio sound daemon"; if (!m_mainloop) { m_mainloop = pa_glib_mainloop_new(nullptr); Q_ASSERT(m_mainloop); } pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop); Q_ASSERT(api); pa_proplist *proplist = pa_proplist_new(); pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, QGuiApplication::applicationDisplayName().toUtf8().constData()); if (!s_applicationId.isEmpty()) { pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, s_applicationId.toUtf8().constData()); } else { pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, QGuiApplication::desktopFileName().toUtf8().constData()); } pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, QGuiApplication::windowIcon().name().toUtf8().constData()); m_context = pa_context_new_with_proplist(api, nullptr, proplist); pa_proplist_free(proplist); Q_ASSERT(m_context); if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) { pa_context_unref(m_context); pa_glib_mainloop_free(m_mainloop); m_context = nullptr; m_mainloop = nullptr; return; } pa_context_set_state_callback(m_context, &context_state_callback, this); } void ContextPrivate::reset() { m_sinks.reset(); m_sinkInputs.reset(); m_sources.reset(); m_sourceOutputs.reset(); m_clients.reset(); m_cards.reset(); m_modules.reset(); m_streamRestores.reset(); m_server->reset(); } bool Context::isValid() { return d->m_context && d->m_mainloop; } QVector Context::sinks() const { return d->m_sinks.data(); } QVector Context::sinkInputs() const { return d->m_sinkInputs.data(); } QVector Context::sources() const { return d->m_sources.data(); } QVector Context::sourceOutputs() const { return d->m_sourceOutputs.data(); } QVector Context::clients() const { return d->m_clients.data(); } QVector Context::cards() const { return d->m_cards.data(); } QVector Context::modules() const { return d->m_modules.data(); } QVector Context::streamRestores() const { return d->m_streamRestores.data(); } Server *Context::server() const { return d->m_server; } void ContextPrivate::setGenericVolume( quint32 index, int channel, qint64 newVolume, pa_cvolume cVolume, const std::function &pa_set_volume) { if (!m_context) { return; } newVolume = qBound(0, newVolume, PA_VOLUME_MAX); pa_cvolume newCVolume = cVolume; if (channel == -1) { // -1 all channels const qint64 diff = newVolume - pa_cvolume_max(&cVolume); for (int i = 0; i < newCVolume.channels; ++i) { newCVolume.values[i] = qBound(0, newCVolume.values[i] + diff, PA_VOLUME_MAX); } } else { Q_ASSERT(newCVolume.channels > channel); newCVolume.values[channel] = newVolume; } if (!pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr)) { qWarning() << "pa_set_volume failed"; return; } } void ContextPrivate::setGenericMute(quint32 index, bool mute, const std::function &pa_set_mute) { if (!m_context) { return; } if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) { qWarning() << "pa_set_mute failed"; return; } } void ContextPrivate::setGenericPort(quint32 index, const QString &portName, const std::function &pa_set_port) { if (!m_context) { return; } if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) { qWarning() << "pa_set_port failed"; return; } } void ContextPrivate::setGenericDeviceForStream( quint32 streamIndex, quint32 deviceIndex, const std::function &pa_move_stream_to_device) { if (!m_context) { return; } if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) { qWarning() << "pa_move_stream_to_device failed"; return; } } void ContextPrivate::setGenericVolumes( quint32 index, QVector channelVolumes, pa_cvolume cVolume, const std::function &pa_set_volume) { if (!m_context) { return; } Q_ASSERT(channelVolumes.count() == cVolume.channels); pa_cvolume newCVolume = cVolume; for (int i = 0; i < channelVolumes.count(); ++i) { newCVolume.values[i] = qBound(0, channelVolumes.at(i), PA_VOLUME_MAX); } if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) { qWarning() << "pa_set_volume failed"; return; } } void Context::setApplicationId(const QString &applicationId) { ContextPrivate::s_applicationId = applicationId; } pa_context *Context::context() const { return d->m_context; } } // PulseAudioQt