summaryrefslogtreecommitdiff
path: root/indra/media_plugins/cef/linux
diff options
context:
space:
mode:
Diffstat (limited to 'indra/media_plugins/cef/linux')
-rw-r--r--indra/media_plugins/cef/linux/volume_catcher_linux.cpp78
-rw-r--r--indra/media_plugins/cef/linux/volume_catcher_linux.h149
-rwxr-xr-xindra/media_plugins/cef/linux/volume_catcher_pipewire.cpp333
-rw-r--r--indra/media_plugins/cef/linux/volume_catcher_pipewire_syms.inc26
-rwxr-xr-xindra/media_plugins/cef/linux/volume_catcher_pulseaudio.cpp322
-rwxr-xr-xindra/media_plugins/cef/linux/volume_catcher_pulseaudio_glib_syms.inc10
-rwxr-xr-xindra/media_plugins/cef/linux/volume_catcher_pulseaudio_syms.inc29
7 files changed, 947 insertions, 0 deletions
diff --git a/indra/media_plugins/cef/linux/volume_catcher_linux.cpp b/indra/media_plugins/cef/linux/volume_catcher_linux.cpp
new file mode 100644
index 0000000000..7d33242063
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_linux.cpp
@@ -0,0 +1,78 @@
+/**
+ * @file volume_catcher.cpp
+ * @brief Linux volume catcher which will pick an implementation to use
+ *
+ * @cond
+ * $LicenseInfo:firstyear=2010&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ * @endcond
+ */
+
+#include "volume_catcher_linux.h"
+
+VolumeCatcher::VolumeCatcher()
+{
+}
+
+void VolumeCatcher::onEnablePipeWireVolumeCatcher(bool enable)
+{
+ if (pimpl != nullptr)
+ return;
+
+ if (enable)
+ {
+ LL_DEBUGS() << "volume catcher using pipewire" << LL_ENDL;
+ pimpl = new VolumeCatcherPipeWire();
+ }
+ else
+ {
+ LL_DEBUGS() << "volume catcher using pulseaudio" << LL_ENDL;
+ pimpl = new VolumeCatcherPulseAudio();
+ }
+}
+
+VolumeCatcher::~VolumeCatcher()
+{
+ if (pimpl != nullptr)
+ {
+ delete pimpl;
+ pimpl = nullptr;
+ }
+}
+
+void VolumeCatcher::setVolume(F32 volume)
+{
+ if (pimpl != nullptr) {
+ pimpl->setVolume(volume);
+ }
+}
+
+void VolumeCatcher::setPan(F32 pan)
+{
+ if (pimpl != nullptr)
+ pimpl->setPan(pan);
+}
+
+void VolumeCatcher::pump()
+{
+ if (pimpl != nullptr)
+ pimpl->pump();
+}
diff --git a/indra/media_plugins/cef/linux/volume_catcher_linux.h b/indra/media_plugins/cef/linux/volume_catcher_linux.h
new file mode 100644
index 0000000000..505f9ffb31
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_linux.h
@@ -0,0 +1,149 @@
+/**
+ * @file volume_catcher_impl.h
+ * @brief
+ *
+ * @cond
+ * $LicenseInfo:firstyear=2010&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ * @endcond
+ */
+
+#ifndef VOLUME_CATCHER_LINUX_H
+#define VOLUME_CATCHER_LINUX_H
+
+#include "linden_common.h"
+
+#include "../volume_catcher.h"
+
+#include <unordered_set>
+#include <mutex>
+
+extern "C" {
+// There's no special reason why we want the *glib* PA mainloop, but the generic polling implementation seems broken.
+#include <pulse/glib-mainloop.h>
+#include <pulse/context.h>
+
+#include <pipewire/pipewire.h>
+
+#include "apr_pools.h"
+#include "apr_dso.h"
+}
+
+#include "media_plugin_base.h"
+
+class VolumeCatcherImpl
+{
+public:
+ virtual ~VolumeCatcherImpl() = default;
+
+ virtual void setVolume(F32 volume) = 0; // 0.0 - 1.0
+
+ // Set the left-right pan of audio sources
+ // where -1.0 = left, 0 = center, and 1.0 = right
+ virtual void setPan(F32 pan) = 0;
+
+ virtual void pump() = 0; // call this at least a few times a second if you can - it affects how quickly we can 'catch' a new audio source and adjust its volume
+};
+
+class VolumeCatcherPulseAudio : public VolumeCatcherImpl
+{
+public:
+ VolumeCatcherPulseAudio();
+ ~VolumeCatcherPulseAudio();
+
+ void setVolume(F32 volume);
+ void setPan(F32 pan);
+ void pump();
+
+ // for internal use - can't be private because used from our C callbacks
+
+ bool loadsyms(std::string pa_dso_name);
+ void init();
+ void cleanup();
+
+ void update_all_volumes(F32 volume);
+ void update_index_volume(U32 index, F32 volume);
+ void connected_okay();
+
+ std::set<U32> mSinkInputIndices;
+ std::map<U32,U32> mSinkInputNumChannels;
+ F32 mDesiredVolume;
+ pa_glib_mainloop *mMainloop;
+ pa_context *mPAContext;
+ bool mConnected;
+ bool mGotSyms;
+};
+
+class VolumeCatcherPipeWire : public VolumeCatcherImpl
+{
+public:
+ VolumeCatcherPipeWire();
+ ~VolumeCatcherPipeWire();
+
+ bool loadsyms(std::string pw_dso_name);
+ void init();
+ void cleanup();
+
+ // some of these should be private
+
+ void lock();
+ void unlock();
+
+ void setVolume(F32 volume);
+ void setPan(F32 pan);
+ void pump();
+
+ void handleRegistryEventGlobal(
+ uint32_t id, uint32_t permissions, const char* type,
+ uint32_t version, const struct spa_dict* props
+ );
+
+ class ChildNode
+ {
+ public:
+ bool mActive = false;
+
+ pw_proxy* mProxy = nullptr;
+ spa_hook mNodeListener {};
+ spa_hook mProxyListener {};
+ VolumeCatcherPipeWire* mImpl = nullptr;
+
+ void updateVolume();
+ void destroy();
+ };
+
+ bool mGotSyms = false;
+
+ F32 mVolume = 1.0f; // max by default
+ // F32 mPan = 0.0f; // center
+
+ pw_thread_loop* mThreadLoop = nullptr;
+ pw_context* mContext = nullptr;
+ pw_core* mCore = nullptr;
+ pw_registry* mRegistry = nullptr;
+ spa_hook mRegistryListener;
+
+ std::unordered_set<ChildNode*> mChildNodes;
+ std::mutex mChildNodesMutex;
+ std::mutex mCleanupMutex;
+};
+
+#endif // VOLUME_CATCHER_LINUX_H
diff --git a/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp b/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp
new file mode 100755
index 0000000000..27fea547c9
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp
@@ -0,0 +1,333 @@
+/**
+ * @file volume_catcher_pipewire.cpp
+ * @brief A Linux-specific, PipeWire-specific hack to detect and volume-adjust new audio sources
+ *
+ * @cond
+ * $LicenseInfo:firstyear=2010&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ * @endcond
+ */
+
+/*
+ The high-level design is as follows:
+ 1) Connect to the PipeWire daemon
+ 2) Find all existing and new audio nodes
+ 3) Examine PID and parent PID's to see if it belongs to our process
+ 4) If so, tell PipeWire to adjust the volume of that node
+ 5) Keep a list of all audio nodes and adjust when we setVolume()
+ */
+
+#include "linden_common.h"
+
+#include "volume_catcher_linux.h"
+
+extern "C" {
+#include <spa/pod/builder.h>
+#include <spa/param/props.h>
+}
+
+SymbolGrabber pwSymbolGrabber;
+
+#include "volume_catcher_pipewire_syms.inc"
+
+////////////////////////////////////////////////////
+
+VolumeCatcherPipeWire::VolumeCatcherPipeWire()
+{
+ init();
+}
+
+VolumeCatcherPipeWire::~VolumeCatcherPipeWire()
+{
+ cleanup();
+}
+
+static void registryEventGlobal(
+ void *data, uint32_t id, uint32_t permissions, const char *type,
+ uint32_t version, const struct spa_dict *props)
+{
+ static_cast<VolumeCatcherPipeWire*>(data)->handleRegistryEventGlobal(
+ id, permissions, type, version, props
+ );
+}
+
+static const struct pw_registry_events REGISTRY_EVENTS = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = registryEventGlobal,
+};
+
+bool VolumeCatcherPipeWire::loadsyms(std::string pw_dso_name)
+{
+ return pwSymbolGrabber.grabSymbols({ pw_dso_name });
+}
+
+void VolumeCatcherPipeWire::init()
+{
+ LL_DEBUGS() << "init" << LL_ENDL;
+
+ mGotSyms = loadsyms("libpipewire-0.3.so.0");
+
+ if (!mGotSyms)
+ return;
+
+ LL_DEBUGS() << "successfully got symbols" << LL_ENDL;
+
+ llpw_init(nullptr, nullptr);
+
+ mThreadLoop = llpw_thread_loop_new("SL Plugin Volume Adjuster", nullptr);
+
+ if (!mThreadLoop)
+ return;
+
+ // i dont think we need to lock this early
+ // std::lock_guard pwLock(*this);
+
+ mContext = llpw_context_new(
+ llpw_thread_loop_get_loop(mThreadLoop), nullptr, 0
+ );
+
+ if (!mContext)
+ return;
+
+ mCore = llpw_context_connect(mContext, nullptr, 0);
+
+ if (!mCore)
+ return;
+
+ mRegistry = pw_core_get_registry(mCore, PW_VERSION_REGISTRY, 0);
+
+ LL_DEBUGS() << "pw_core_get_registry: " << (mRegistry?"success":"nullptr") << LL_ENDL;
+
+ spa_zero(mRegistryListener);
+
+ pw_registry_add_listener(
+ mRegistry, &mRegistryListener, &REGISTRY_EVENTS, this
+ );
+
+ llpw_thread_loop_start(mThreadLoop);
+
+ LL_DEBUGS() << "thread loop started" << LL_ENDL;
+}
+
+void VolumeCatcherPipeWire::cleanup()
+{
+ {
+ std::unique_lock childNodesLock(mChildNodesMutex);
+ for (auto *childNode: mChildNodes)
+ childNode->destroy();
+
+ mChildNodes.clear();
+ }
+
+ {
+ std::unique_lock pwLock(mCleanupMutex);
+ if (mRegistry)
+ llpw_proxy_destroy((struct pw_proxy *) mRegistry);
+
+ spa_zero(mRegistryListener);
+
+ if (mCore)
+ llpw_core_disconnect(mCore);
+ if (mContext)
+ llpw_context_destroy(mContext);
+ }
+
+ if (!mThreadLoop)
+ return;
+
+ llpw_thread_loop_stop(mThreadLoop);
+ llpw_thread_loop_destroy(mThreadLoop);
+
+ LL_DEBUGS() << "cleanup done" << LL_ENDL;
+}
+
+void VolumeCatcherPipeWire::lock()
+{
+ if (!mThreadLoop)
+ return;
+
+ llpw_thread_loop_lock(mThreadLoop);
+}
+
+void VolumeCatcherPipeWire::unlock()
+{
+ if (!mThreadLoop)
+ return;
+
+ llpw_thread_loop_unlock(mThreadLoop);
+}
+
+const uint32_t channels = 1;
+const float resetVolumes[channels] = { 1.0f };
+
+void VolumeCatcherPipeWire::ChildNode::updateVolume()
+{
+ if (!mActive)
+ return;
+
+ F32 volume = std::clamp(mImpl->mVolume, 0.0f, 1.0f);
+
+ const float volumes[channels] = { volume };
+
+ uint8_t buffer[512];
+
+ spa_pod_builder builder;
+ spa_pod_builder_init(&builder, buffer, sizeof(buffer));
+
+ spa_pod_frame frame;
+ spa_pod_builder_push_object(&builder, &frame, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+
+ // resets system-wide memorized volume for chromium (not google chrome) to 100%
+ spa_pod_builder_prop(&builder, SPA_PROP_channelVolumes, 0);
+ spa_pod_builder_array(&builder, sizeof(float), SPA_TYPE_Float, channels, resetVolumes);
+
+ // sets temporary volume
+ spa_pod_builder_prop(&builder, SPA_PROP_softVolumes, 0);
+ spa_pod_builder_array(&builder, sizeof(float), SPA_TYPE_Float, channels, volumes);
+
+ spa_pod* pod = static_cast<spa_pod*>(spa_pod_builder_pop(&builder, &frame));
+
+ {
+ std::lock_guard pwLock(*mImpl);
+ pw_node_set_param(mProxy, SPA_PARAM_Props, 0, pod);
+ }
+}
+
+void VolumeCatcherPipeWire::ChildNode::destroy()
+{
+ if (!mActive)
+ return;
+
+ mActive = false;
+
+ {
+ std::unique_lock childNodesLock(mImpl->mChildNodesMutex);
+ mImpl->mChildNodes.erase(this);
+ }
+
+ spa_hook_remove(&mNodeListener);
+ spa_hook_remove(&mProxyListener);
+
+ {
+ std::lock_guard pwLock(*mImpl);
+ llpw_proxy_destroy(mProxy);
+ }
+}
+
+static void nodeEventInfo(void* data, const struct pw_node_info* info)
+{
+ const char* processId = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_ID);
+
+ if (processId == nullptr)
+ return;
+
+ pid_t pid = atoll(processId);
+
+ if (!isPluginPid(pid))
+ return;
+
+ const char* appName = spa_dict_lookup(info->props, PW_KEY_APP_NAME);
+ LL_DEBUGS() << "got app: " << appName << LL_ENDL;
+
+ auto* const childNode = static_cast<VolumeCatcherPipeWire::ChildNode*>(data);
+ LL_DEBUGS() << "init volume: " << childNode->mImpl->mVolume << LL_ENDL;
+
+ childNode->updateVolume();
+
+ {
+ std::lock_guard childNodesLock(childNode->mImpl->mChildNodesMutex);
+ childNode->mImpl->mChildNodes.insert(childNode);
+ }
+}
+
+static const struct pw_node_events NODE_EVENTS = {
+ .version = PW_VERSION_CLIENT_EVENTS,
+ .info = nodeEventInfo,
+};
+
+static void proxyEventDestroy(void* data)
+{
+ auto* const childNode = static_cast<VolumeCatcherPipeWire::ChildNode*>(data);
+ childNode->destroy();
+}
+
+static void proxyEventRemoved(void* data)
+{
+ auto* const childNode = static_cast<VolumeCatcherPipeWire::ChildNode*>(data);
+ childNode->destroy();
+}
+
+static const struct pw_proxy_events PROXY_EVENTS = {
+ .version = PW_VERSION_PROXY_EVENTS,
+ .destroy = proxyEventDestroy,
+ .removed = proxyEventRemoved,
+};
+
+void VolumeCatcherPipeWire::handleRegistryEventGlobal(
+ uint32_t id, uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ if (props == nullptr || type == nullptr || strcmp(type, PW_TYPE_INTERFACE_Node) != 0)
+ return;
+
+ const char* mediaClass = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+
+ if (mediaClass == nullptr || strcmp(mediaClass, "Stream/Output/Audio") != 0)
+ return;
+
+ pw_proxy* proxy = static_cast<pw_proxy*>(
+ pw_registry_bind(mRegistry, id, type, PW_VERSION_CLIENT, sizeof(ChildNode))
+ );
+
+ auto* const childNode = static_cast<ChildNode*>(llpw_proxy_get_user_data(proxy));
+
+ childNode->mActive = true;
+ childNode->mProxy = proxy;
+ childNode->mImpl = this;
+
+ pw_node_add_listener(proxy, &childNode->mNodeListener, &NODE_EVENTS, childNode);
+ llpw_proxy_add_listener(proxy, &childNode->mProxyListener, &PROXY_EVENTS, childNode);
+}
+
+void VolumeCatcherPipeWire::setVolume(F32 volume)
+{
+ LL_DEBUGS() << "setting volume to: " << volume << LL_ENDL;
+
+ mVolume = volume;
+
+ {
+ std::unique_lock childNodeslock(mChildNodesMutex);
+ std::unordered_set<ChildNode *> copyOfChildNodes(mChildNodes);
+
+ LL_DEBUGS() << "found " << copyOfChildNodes.size() << " child nodes" << LL_ENDL;
+
+ for (auto* childNode : copyOfChildNodes)
+ childNode->updateVolume();
+ }
+}
+
+void VolumeCatcherPipeWire::setPan(F32 pan)
+{
+}
+
+void VolumeCatcherPipeWire::pump()
+{
+}
diff --git a/indra/media_plugins/cef/linux/volume_catcher_pipewire_syms.inc b/indra/media_plugins/cef/linux/volume_catcher_pipewire_syms.inc
new file mode 100644
index 0000000000..dbc0f5f169
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_pipewire_syms.inc
@@ -0,0 +1,26 @@
+#define G pwSymbolGrabber
+
+// required symbols to grab
+LL_GRAB_SYM(G, true, pw_init, void, int *argc, char **argv[]);
+// LL_GRAB_SYM(G, true, pw_main_loop_new, struct pw_main_loop *, const struct spa_dict *props);
+// LL_GRAB_SYM(G, true, pw_main_loop_get_loop, struct pw_loop *, struct pw_main_loop *loop);
+// LL_GRAB_SYM(G, true, pw_main_loop_destroy, void, struct pw_main_loop *loop);
+// LL_GRAB_SYM(G, true, pw_main_loop_run, void, struct pw_main_loop *loop);
+LL_GRAB_SYM(G, true, pw_context_new, struct pw_context *, struct pw_loop *main_loop, struct pw_properties *props, size_t user_data_size);
+LL_GRAB_SYM(G, true, pw_context_destroy, void, struct pw_context *context);
+LL_GRAB_SYM(G, true, pw_context_connect, struct pw_core *, struct pw_context *context, struct pw_properties *properties, size_t user_data_size);
+LL_GRAB_SYM(G, true, pw_thread_loop_new, struct pw_thread_loop *, const char *name, const struct spa_dict *props);
+LL_GRAB_SYM(G, true, pw_thread_loop_destroy, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(G, true, pw_thread_loop_get_loop, struct pw_loop *, struct pw_thread_loop *loop);
+LL_GRAB_SYM(G, true, pw_thread_loop_start, int, struct pw_thread_loop *loop);
+LL_GRAB_SYM(G, true, pw_thread_loop_stop, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(G, true, pw_thread_loop_lock, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(G, true, pw_thread_loop_unlock, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(G, true, pw_proxy_add_listener, void, struct pw_proxy *proxy, struct spa_hook *listener, const struct pw_proxy_events *events, void *data);
+LL_GRAB_SYM(G, true, pw_proxy_destroy, void, struct pw_proxy *proxy);
+LL_GRAB_SYM(G, true, pw_proxy_get_user_data, void *, struct pw_proxy *proxy);
+LL_GRAB_SYM(G, true, pw_core_disconnect, int, struct pw_core *core);
+
+// optional symbols to grab
+
+#undef G
diff --git a/indra/media_plugins/cef/linux/volume_catcher_pulseaudio.cpp b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio.cpp
new file mode 100755
index 0000000000..9417c49d38
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio.cpp
@@ -0,0 +1,322 @@
+/**
+ * @file volume_catcher_pulseaudio.cpp
+ * @brief A Linux-specific, PulseAudio-specific hack to detect and volume-adjust new audio sources
+ *
+ * @cond
+ * $LicenseInfo:firstyear=2010&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ * @endcond
+ */
+
+/*
+ The high-level design is as follows:
+ 1) Connect to the PulseAudio daemon
+ 2) Watch for the creation of new audio players connecting to the daemon (this includes ALSA clients running on the PulseAudio emulation layer, such as Flash plugins)
+ 3) Examine any new audio player's PID to see if it belongs to our own process
+ 4) If so, tell PA to adjust the volume of that audio player ('sink input' in PA parlance)
+ 5) Keep a list of all living audio players that we care about, adjust the volumes of all of them when we get a new setVolume() call
+ */
+
+#include "linden_common.h"
+
+#include "volume_catcher_linux.h"
+
+extern "C" {
+#include <glib.h>
+#include <glib-object.h>
+
+#include <pulse/introspect.h>
+
+#include <pulse/subscribe.h>
+}
+
+SymbolGrabber paSymbolGrabber;
+
+#include "volume_catcher_pulseaudio_syms.inc"
+#include "volume_catcher_pulseaudio_glib_syms.inc"
+
+////////////////////////////////////////////////////
+
+// PulseAudio requires a chain of callbacks with C linkage
+extern "C" {
+ void callback_discovered_sinkinput(pa_context *context, const pa_sink_input_info *i, int eol, void *userdata);
+ void callback_subscription_alert(pa_context *context, pa_subscription_event_type_t t, uint32_t index, void *userdata);
+ void callback_context_state(pa_context *context, void *userdata);
+}
+
+VolumeCatcherPulseAudio::VolumeCatcherPulseAudio()
+ : mDesiredVolume(0.0f),
+ mMainloop(nullptr),
+ mPAContext(nullptr),
+ mConnected(false),
+ mGotSyms(false)
+{
+ init();
+}
+
+VolumeCatcherPulseAudio::~VolumeCatcherPulseAudio()
+{
+ cleanup();
+}
+
+bool VolumeCatcherPulseAudio::loadsyms(std::string pulse_dso_name)
+{
+ return paSymbolGrabber.grabSymbols({ pulse_dso_name });
+}
+
+void VolumeCatcherPulseAudio::init()
+{
+ // try to be as defensive as possible because PA's interface is a
+ // bit fragile and (for our purposes) we'd rather simply not function
+ // than crash
+
+ // we cheat and rely upon libpulse-mainloop-glib.so.0 to pull-in
+ // libpulse.so.0 - this isn't a great assumption, and the two DSOs should
+ // probably be loaded separately. Our Linux DSO framework needs refactoring,
+ // we do this sort of thing a lot with practically identical logic...
+ mGotSyms = loadsyms("libpulse-mainloop-glib.so.0");
+
+ if (!mGotSyms)
+ mGotSyms = loadsyms("libpulse.so.0");
+
+ if (!mGotSyms)
+ return;
+
+ mMainloop = llpa_glib_mainloop_new(g_main_context_default());
+
+ if (mMainloop)
+ {
+ pa_mainloop_api *api = llpa_glib_mainloop_get_api(mMainloop);
+
+ if (api)
+ {
+ pa_proplist *proplist = llpa_proplist_new();
+
+ if (proplist)
+ {
+ llpa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "multimedia-player");
+ llpa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "com.secondlife.viewer.mediaplugvoladjust");
+ llpa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "SL Plugin Volume Adjuster");
+ llpa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, "1");
+
+ // plain old pa_context_new() is broken!
+ mPAContext = llpa_context_new_with_proplist(api, nullptr, proplist);
+
+ llpa_proplist_free(proplist);
+ }
+ }
+ }
+
+ // Now we've set up a PA context and mainloop, try connecting the
+ // PA context to a PA daemon.
+ if (mPAContext)
+ {
+ llpa_context_set_state_callback(mPAContext, callback_context_state, this);
+ pa_context_flags_t cflags = (pa_context_flags)0; // maybe add PA_CONTEXT_NOAUTOSPAWN?
+ if (llpa_context_connect(mPAContext, nullptr, cflags, nullptr) >= 0)
+ {
+ // Okay! We haven't definitely connected, but we
+ // haven't definitely failed yet.
+ }
+ else
+ {
+ // Failed to connect to PA manager... we'll leave
+ // things like that. Perhaps we should try again later.
+ }
+ }
+}
+
+void VolumeCatcherPulseAudio::cleanup()
+{
+ mConnected = false;
+
+ if (mGotSyms && mPAContext)
+ {
+ llpa_context_disconnect(mPAContext);
+ llpa_context_unref(mPAContext);
+ }
+
+ mPAContext = nullptr;
+
+ if (mGotSyms && mMainloop)
+ llpa_glib_mainloop_free(mMainloop);
+
+ mMainloop = nullptr;
+}
+
+void VolumeCatcherPulseAudio::setVolume(F32 volume)
+{
+ mDesiredVolume = volume;
+
+ if (!mGotSyms)
+ return;
+
+ if (mConnected && mPAContext)
+ {
+ update_all_volumes(mDesiredVolume);
+ }
+
+ pump();
+}
+
+void VolumeCatcherPulseAudio::setPan(F32 pan)
+{
+}
+
+void VolumeCatcherPulseAudio::pump()
+{
+ gboolean may_block = FALSE;
+ g_main_context_iteration(g_main_context_default(), may_block);
+}
+
+void VolumeCatcherPulseAudio::connected_okay()
+{
+ pa_operation *op;
+
+ // fetch global list of existing sinkinputs
+ if ((op = llpa_context_get_sink_input_info_list(mPAContext,
+ callback_discovered_sinkinput,
+ this)))
+ {
+ llpa_operation_unref(op);
+ }
+
+ // subscribe to future global sinkinput changes
+ llpa_context_set_subscribe_callback(mPAContext,
+ callback_subscription_alert,
+ this);
+ if ((op = llpa_context_subscribe(mPAContext, (pa_subscription_mask_t)
+ (PA_SUBSCRIPTION_MASK_SINK_INPUT),
+ nullptr, nullptr)))
+ {
+ llpa_operation_unref(op);
+ }
+}
+
+void VolumeCatcherPulseAudio::update_all_volumes(F32 volume)
+{
+ for (std::set<U32>::iterator it = mSinkInputIndices.begin();
+ it != mSinkInputIndices.end(); ++it)
+ {
+ update_index_volume(*it, volume);
+ }
+}
+
+void VolumeCatcherPulseAudio::update_index_volume(U32 index, F32 volume)
+{
+ static pa_cvolume cvol;
+ llpa_cvolume_set(&cvol, mSinkInputNumChannels[index],
+ llpa_sw_volume_from_linear(volume));
+
+ pa_context *c = mPAContext;
+ uint32_t idx = index;
+ const pa_cvolume *cvolumep = &cvol;
+ pa_context_success_cb_t cb = nullptr; // okay as null
+ void *userdata = nullptr; // okay as null
+
+ pa_operation *op;
+ if ((op = llpa_context_set_sink_input_volume(c, idx, cvolumep, cb, userdata)))
+ llpa_operation_unref(op);
+}
+
+void callback_discovered_sinkinput(pa_context *context, const pa_sink_input_info *sii, int eol, void *userdata)
+{
+ VolumeCatcherPulseAudio *impl = dynamic_cast<VolumeCatcherPulseAudio*>((VolumeCatcherPulseAudio*)userdata);
+ llassert(impl);
+
+ if (0 == eol)
+ {
+ pa_proplist *proplist = sii->proplist;
+ pid_t sinkpid = atoll(llpa_proplist_gets(proplist, PA_PROP_APPLICATION_PROCESS_ID));
+
+ if (isPluginPid( sinkpid )) // does the discovered sinkinput belong to this process?
+ {
+ bool is_new = (impl->mSinkInputIndices.find(sii->index) == impl->mSinkInputIndices.end());
+
+ impl->mSinkInputIndices.insert(sii->index);
+ impl->mSinkInputNumChannels[sii->index] = sii->channel_map.channels;
+
+ if (is_new)
+ {
+ // new!
+ impl->update_index_volume(sii->index, impl->mDesiredVolume);
+ }
+ else
+ {
+ // seen it already, do nothing.
+ }
+ }
+ }
+}
+
+void callback_subscription_alert(pa_context *context, pa_subscription_event_type_t t, uint32_t index, void *userdata)
+{
+ VolumeCatcherPulseAudio *impl = dynamic_cast<VolumeCatcherPulseAudio*>((VolumeCatcherPulseAudio*)userdata);
+ llassert(impl);
+
+ switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK)
+ {
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
+ {
+ // forget this sinkinput, if we were caring about it
+ impl->mSinkInputIndices.erase(index);
+ impl->mSinkInputNumChannels.erase(index);
+ }
+ else if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW)
+ {
+ // ask for more info about this new sinkinput
+ pa_operation *op;
+ if ((op = llpa_context_get_sink_input_info(impl->mPAContext, index, callback_discovered_sinkinput, impl)))
+ {
+ llpa_operation_unref(op);
+ }
+ }
+ else
+ {
+ // property change on this sinkinput - we don't care.
+ }
+ break;
+
+ default:;
+ }
+}
+
+void callback_context_state(pa_context *context, void *userdata)
+{
+ VolumeCatcherPulseAudio *impl = dynamic_cast<VolumeCatcherPulseAudio*>((VolumeCatcherPulseAudio*)userdata);
+ llassert(impl);
+
+ switch (llpa_context_get_state(context))
+ {
+ case PA_CONTEXT_READY:
+ impl->mConnected = true;
+ impl->connected_okay();
+ break;
+ case PA_CONTEXT_TERMINATED:
+ impl->mConnected = false;
+ break;
+ case PA_CONTEXT_FAILED:
+ impl->mConnected = false;
+ break;
+ default:;
+ }
+}
diff --git a/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_glib_syms.inc b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_glib_syms.inc
new file mode 100755
index 0000000000..e9b7196e51
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_glib_syms.inc
@@ -0,0 +1,10 @@
+#define G paSymbolGrabber
+
+// required symbols to grab
+LL_GRAB_SYM(G, true, pa_glib_mainloop_free, void, pa_glib_mainloop* g)
+LL_GRAB_SYM(G, true, pa_glib_mainloop_get_api, pa_mainloop_api*, pa_glib_mainloop* g)
+LL_GRAB_SYM(G, true, pa_glib_mainloop_new, pa_glib_mainloop *, GMainContext *c)
+
+// optional symbols to grab
+
+#undef G
diff --git a/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_syms.inc b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_syms.inc
new file mode 100755
index 0000000000..4859a34405
--- /dev/null
+++ b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_syms.inc
@@ -0,0 +1,29 @@
+#define G paSymbolGrabber
+
+// required symbols to grab
+LL_GRAB_SYM(G, true, pa_context_connect, int, pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api)
+LL_GRAB_SYM(G, true, pa_context_disconnect, void, pa_context *c)
+LL_GRAB_SYM(G, true, pa_context_get_sink_input_info, pa_operation*, pa_context *c, uint32_t idx, pa_sink_input_info_cb_t cb, void *userdata)
+LL_GRAB_SYM(G, true, pa_context_get_sink_input_info_list, pa_operation*, pa_context *c, pa_sink_input_info_cb_t cb, void *userdata)
+LL_GRAB_SYM(G, true, pa_context_get_state, pa_context_state_t, pa_context *c)
+LL_GRAB_SYM(G, true, pa_context_new_with_proplist, pa_context*, pa_mainloop_api *mainloop, const char *name, pa_proplist *proplist)
+LL_GRAB_SYM(G, true, pa_context_set_sink_input_volume, pa_operation*, pa_context *c, uint32_t idx, const pa_cvolume *volume, pa_context_success_cb_t cb, void *userdata)
+LL_GRAB_SYM(G, true, pa_context_set_state_callback, void, pa_context *c, pa_context_notify_cb_t cb, void *userdata)
+LL_GRAB_SYM(G, true, pa_context_set_subscribe_callback, void, pa_context *c, pa_context_subscribe_cb_t cb, void *userdata)
+LL_GRAB_SYM(G, true, pa_context_subscribe, pa_operation*, pa_context *c, pa_subscription_mask_t m, pa_context_success_cb_t cb, void *userdata)
+LL_GRAB_SYM(G, true, pa_context_unref, void, pa_context *c)
+LL_GRAB_SYM(G, true, pa_cvolume_set, pa_cvolume*, pa_cvolume *a, unsigned channels, pa_volume_t v)
+LL_GRAB_SYM(G, true, pa_operation_unref, void, pa_operation *o)
+LL_GRAB_SYM(G, true, pa_proplist_free, void, pa_proplist* p)
+LL_GRAB_SYM(G, true, pa_proplist_gets, const char*, pa_proplist *p, const char *key)
+LL_GRAB_SYM(G, true, pa_proplist_new, pa_proplist*, void)
+LL_GRAB_SYM(G, true, pa_proplist_sets, int, pa_proplist *p, const char *key, const char *value)
+LL_GRAB_SYM(G, true, pa_sw_volume_from_linear, pa_volume_t, double v)
+// LL_GRAB_SYM(G, true, pa_mainloop_free, void, pa_mainloop *m)
+// LL_GRAB_SYM(G, true, pa_mainloop_get_api, pa_mainloop_api *, pa_mainloop *m)
+// LL_GRAB_SYM(G, true, pa_mainloop_iterate, int, pa_mainloop *m, int block, int *retval)
+// LL_GRAB_SYM(G, true, pa_mainloop_new, pa_mainloop *, void)
+
+// optional symbols to grab
+
+#undef G