summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
authorMaki <maki@hotmilk.space>2024-04-17 23:06:29 -0400
committerMaki <maki@hotmilk.space>2024-04-17 23:06:29 -0400
commit1e5b77547fd4a70888ab24834bfdb634206cafc3 (patch)
tree4d32017ffc78b955992dc65da43ba0f79e986e59 /indra
parent7d2608947ccfa2215fa66d162134bf67bf27b383 (diff)
Use PipeWire for Linux volume catcher
Diffstat (limited to 'indra')
-rw-r--r--indra/cmake/FindPipeWire.cmake100
-rw-r--r--indra/media_plugins/cef/CMakeLists.txt26
-rwxr-xr-xindra/media_plugins/cef/linux_volume_catcher_pa.cpp (renamed from indra/media_plugins/cef/linux_volume_catcher.cpp)2
-rwxr-xr-xindra/media_plugins/cef/linux_volume_catcher_pw.cpp451
-rw-r--r--indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc22
5 files changed, 589 insertions, 12 deletions
diff --git a/indra/cmake/FindPipeWire.cmake b/indra/cmake/FindPipeWire.cmake
new file mode 100644
index 0000000000..868acf5ec1
--- /dev/null
+++ b/indra/cmake/FindPipeWire.cmake
@@ -0,0 +1,100 @@
+# cmake-format: off
+# .rst: FindPipeWire
+# -------
+#
+# Try to find PipeWire on a Unix system.
+#
+# This will define the following variables:
+#
+# ``PIPEWIRE_FOUND`` True if (the requested version of) PipeWire is available
+# ``PIPEWIRE_VERSION`` The version of PipeWire ``PIPEWIRE_LIBRARIES`` This can
+# be passed to target_link_libraries() instead of the ``PipeWire::PipeWire``
+# target ``PIPEWIRE_INCLUDE_DIRS`` This should be passed to
+# target_include_directories() if the target is not used for linking
+# ``PIPEWIRE_COMPILE_FLAGS`` This should be passed to target_compile_options()
+# if the target is not used for linking
+#
+# If ``PIPEWIRE_FOUND`` is TRUE, it will also define the following imported
+# target:
+#
+# ``PipeWire::PipeWire`` The PipeWire library
+#
+# In general we recommend using the imported target, as it is easier to use.
+# Bear in mind, however, that if the target is in the link interface of an
+# exported library, it must be made available by the package config file.
+
+# =============================================================================
+# Copyright 2014 Alex Merry <alex.merry@kde.org> Copyright 2014 Martin Gräßlin
+# <mgraesslin@kde.org> Copyright 2018-2020 Jan Grulich <jgrulich@redhat.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the copyright notice, this list
+# of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or
+# other materials provided with the distribution.
+# 3. The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# =============================================================================
+# cmake-format: on
+
+# Use pkg-config to get the directories and then use these values in the FIND_PATH() and FIND_LIBRARY() calls
+find_package(PkgConfig QUIET)
+
+pkg_search_module(PKG_PIPEWIRE QUIET libpipewire-0.3)
+pkg_search_module(PKG_SPA QUIET libspa-0.2)
+
+set(PIPEWIRE_COMPILE_FLAGS "${PKG_PIPEWIRE_CFLAGS}" "${PKG_SPA_CFLAGS}")
+set(PIPEWIRE_VERSION "${PKG_PIPEWIRE_VERSION}")
+
+find_path(
+ PIPEWIRE_INCLUDE_DIRS
+ NAMES pipewire/pipewire.h
+ HINTS ${PKG_PIPEWIRE_INCLUDE_DIRS} ${PKG_PIPEWIRE_INCLUDE_DIRS}/pipewire-0.3)
+
+find_path(
+ SPA_INCLUDE_DIRS
+ NAMES spa/param/props.h
+ HINTS ${PKG_SPA_INCLUDE_DIRS} ${PKG_SPA_INCLUDE_DIRS}/spa-0.2)
+
+find_library(
+ PIPEWIRE_LIBRARIES
+ NAMES pipewire-0.3
+ HINTS ${PKG_PIPEWIRE_LIBRARY_DIRS})
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(
+ PipeWire
+ FOUND_VAR PIPEWIRE_FOUND
+ REQUIRED_VARS PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS SPA_INCLUDE_DIRS
+ VERSION_VAR PIPEWIRE_VERSION)
+
+if(PIPEWIRE_FOUND AND NOT TARGET PipeWire::PipeWire)
+ add_library(PipeWire::PipeWire UNKNOWN IMPORTED)
+ set_target_properties(
+ PipeWire::PipeWire
+ PROPERTIES IMPORTED_LOCATION "${PIPEWIRE_LIBRARIES}"
+ INTERFACE_COMPILE_OPTIONS "${PIPEWIRE_COMPILE_FLAGS}"
+ INTERFACE_INCLUDE_DIRECTORIES "${PIPEWIRE_INCLUDE_DIRS};${SPA_INCLUDE_DIRS}")
+endif()
+
+mark_as_advanced(PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS)
+
+include(FeatureSummary)
+set_package_properties(
+ PipeWire PROPERTIES
+ URL "https://www.pipewire.org"
+ DESCRIPTION "PipeWire - multimedia processing")
diff --git a/indra/media_plugins/cef/CMakeLists.txt b/indra/media_plugins/cef/CMakeLists.txt
index bbd2eb222a..9e129f2185 100644
--- a/indra/media_plugins/cef/CMakeLists.txt
+++ b/indra/media_plugins/cef/CMakeLists.txt
@@ -24,17 +24,21 @@ set(media_plugin_cef_HEADER_FILES
# Select which VolumeCatcher implementation to use
if (LINUX)
- foreach( PULSE_FILE pulse/introspect.h pulse/context.h pulse/subscribe.h pulse/glib-mainloop.h )
- find_path( PULSE_FILE_${PULSE_FILE}_FOUND ${PULSE_FILE} NO_CACHE)
- if( NOT PULSE_FILE_${PULSE_FILE}_FOUND )
- message( "Looking for ${PULSE_FILE} ... not found")
- message( FATAL_ERROR "Pulse header not found" )
- else()
- message( "Looking for ${PULSE_FILE} ... found")
- endif()
- endforeach()
- message( "Building with linux volume catcher" )
- set(LINUX_VOLUME_CATCHER linux_volume_catcher.cpp)
+ # foreach( PULSE_FILE pulse/introspect.h pulse/context.h pulse/subscribe.h pulse/glib-mainloop.h )
+ # find_path( PULSE_FILE_${PULSE_FILE}_FOUND ${PULSE_FILE} NO_CACHE)
+ # if( NOT PULSE_FILE_${PULSE_FILE}_FOUND )
+ # message( "Looking for ${PULSE_FILE} ... not found")
+ # message( FATAL_ERROR "Pulse header not found" )
+ # else()
+ # message( "Looking for ${PULSE_FILE} ... found")
+ # endif()
+ # endforeach()
+
+ include(FindPipeWire)
+ include_directories(SYSTEM ${PIPEWIRE_INCLUDE_DIRS} ${SPA_INCLUDE_DIRS})
+
+ message( "Building with Linux volume catcher using PipeWire" )
+ set(LINUX_VOLUME_CATCHER linux_volume_catcher_pw.cpp)
list(APPEND media_plugin_cef_SOURCE_FILES ${LINUX_VOLUME_CATCHER})
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--build-id -Wl,-rpath,'$ORIGIN:$ORIGIN/../../lib'")
diff --git a/indra/media_plugins/cef/linux_volume_catcher.cpp b/indra/media_plugins/cef/linux_volume_catcher_pa.cpp
index 9e6fe461a6..80f58954d4 100755
--- a/indra/media_plugins/cef/linux_volume_catcher.cpp
+++ b/indra/media_plugins/cef/linux_volume_catcher_pa.cpp
@@ -1,5 +1,5 @@
/**
- * @file linux_volume_catcher.cpp
+ * @file linux_volume_catcher_pa.cpp
* @brief A Linux-specific, PulseAudio-specific hack to detect and volume-adjust new audio sources
*
* @cond
diff --git a/indra/media_plugins/cef/linux_volume_catcher_pw.cpp b/indra/media_plugins/cef/linux_volume_catcher_pw.cpp
new file mode 100755
index 0000000000..b7171d10f9
--- /dev/null
+++ b/indra/media_plugins/cef/linux_volume_catcher_pw.cpp
@@ -0,0 +1,451 @@
+/**
+ * @file linux_volume_catcher_pw.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.h"
+
+#include <unordered_set>
+#include <mutex>
+
+extern "C" {
+#include <pipewire/pipewire.h>
+#include <spa/pod/builder.h>
+#include <spa/param/props.h>
+
+#include "apr_pools.h"
+#include "apr_dso.h"
+}
+
+#include "media_plugin_base.h"
+
+SymbolGrabber gSymbolGrabber;
+
+#include "linux_volume_catcher_pw_syms.inc"
+
+////////////////////////////////////////////////////
+
+class VolumeCatcherImpl
+{
+public:
+ VolumeCatcherImpl();
+ ~VolumeCatcherImpl();
+
+ bool loadsyms(std::string pw_dso_name);
+ void init();
+ void cleanup();
+
+ void pwLock();
+ void pwUnlock();
+
+ void setVolume(F32 volume);
+ // void setPan(F32 pan);
+
+ 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 {};
+ VolumeCatcherImpl* 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;
+ pw_context* mContext;
+ pw_core* mCore;
+ pw_registry* mRegistry;
+ spa_hook mRegistryListener;
+
+ std::unordered_set<ChildNode*> mChildNodes;
+ std::mutex mChildNodesMutex;
+};
+
+VolumeCatcherImpl::VolumeCatcherImpl()
+{
+ init();
+}
+
+VolumeCatcherImpl::~VolumeCatcherImpl()
+{
+ cleanup();
+}
+
+// static void debugClear()
+// {
+// auto file = fopen("/home/maki/git/firestorm-viewer/log.txt", "w");
+// fprintf(file, "\n");
+// fclose(file);
+// }
+
+// static void debugPrint(const char* format, ...)
+// {
+// va_list args;
+// va_start(args, format);
+// auto file = fopen("/home/maki/git/firestorm-viewer/log.txt", "a");
+// vfprintf(file, format, args);
+// fclose(file);
+// va_end(args);
+// }
+
+static void registryEventGlobal(
+ void *data, uint32_t id, uint32_t permissions, const char *type,
+ uint32_t version, const struct spa_dict *props
+)
+{
+ static_cast<VolumeCatcherImpl*>(data)->handleRegistryEventGlobal(
+ id, permissions, type, version, props
+ );
+}
+
+static const struct pw_registry_events REGISTRY_EVENTS = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = registryEventGlobal,
+};
+
+bool VolumeCatcherImpl::loadsyms(std::string pw_dso_name)
+{
+ return gSymbolGrabber.grabSymbols({ pw_dso_name });
+}
+
+void VolumeCatcherImpl::init()
+{
+ // debugClear();
+ // debugPrint("init\n");
+
+ mGotSyms = loadsyms("libpipewire-0.3.so.0");
+ if (!mGotSyms) return;
+
+ llpw_init(NULL, NULL);
+
+ mThreadLoop = llpw_thread_loop_new("SL Plugin Volume Adjuster", NULL);
+ if (!mThreadLoop) return;
+
+ pwLock();
+
+ mContext = llpw_context_new(
+ llpw_thread_loop_get_loop(mThreadLoop), NULL, 0
+ );
+ if (!mContext) return pwUnlock();
+
+ mCore = llpw_context_connect(mContext, NULL, 0);
+ if (!mCore) return pwUnlock();
+
+ mRegistry = pw_core_get_registry(mCore, PW_VERSION_REGISTRY, 0);
+
+ // debugPrint("got registry\n");
+
+ spa_zero(mRegistryListener);
+
+ pw_registry_add_listener(
+ mRegistry, &mRegistryListener, &REGISTRY_EVENTS, this
+ );
+
+ llpw_thread_loop_start(mThreadLoop);
+
+ pwUnlock();
+
+ // debugPrint("started thread loop\n");
+}
+
+void VolumeCatcherImpl::cleanup()
+{
+ mChildNodesMutex.lock();
+ for (auto* childNode : mChildNodes) {
+ childNode->destroy();
+ }
+ mChildNodes.clear();
+ mChildNodesMutex.unlock();
+
+ pwLock();
+ if (mRegistry) llpw_proxy_destroy((struct pw_proxy*)mRegistry);
+ spa_zero(mRegistryListener);
+ if (mCore) llpw_core_disconnect(mCore);
+ if (mContext) llpw_context_destroy(mContext);
+ pwUnlock();
+
+ if (!mThreadLoop) return;
+
+ llpw_thread_loop_stop(mThreadLoop);
+ llpw_thread_loop_destroy(mThreadLoop);
+
+ // debugPrint("cleanup done\n");
+}
+
+void VolumeCatcherImpl::pwLock() {
+ if (!mThreadLoop) return;
+ llpw_thread_loop_lock(mThreadLoop);
+}
+
+void VolumeCatcherImpl::pwUnlock() {
+ if (!mThreadLoop) return;
+ llpw_thread_loop_unlock(mThreadLoop);
+}
+
+// #define INV_LERP(a, b, v) (v - a) / (b - a)
+
+// #include <sys/time.h>
+
+void VolumeCatcherImpl::ChildNode::updateVolume()
+{
+ if (!mActive) return;
+
+ F32 volume = std::clamp(mImpl->mVolume, 0.0f, 1.0f);
+ // F32 pan = std::clamp(mImpl->mPan, -1.0f, 1.0f);
+
+ // debugClear();
+ // struct timeval time;
+ // gettimeofday(&time, NULL);
+ // double t = (double)time.tv_sec + (double)(time.tv_usec / 1000) / 1000;
+ // debugPrint("time is %f\n", t);
+ // F32 pan = std::sin(t * 2.0d);
+ // debugPrint("pan is %f\n", pan);
+
+ // uint32_t channels = 2;
+ // float volumes[channels];
+ // volumes[1] = INV_LERP(-1.0f, 0.0f, pan) * volume; // left
+ // volumes[0] = INV_LERP(1.0f, 0.0f, pan) * volume; // right
+
+ uint32_t channels = 1;
+ float volumes[channels];
+ volumes[0] = 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);
+ spa_pod_builder_prop(&builder, SPA_PROP_channelVolumes, 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));
+
+ mImpl->pwLock();
+ pw_node_set_param(mProxy, SPA_PARAM_Props, 0, pod);
+ mImpl->pwUnlock();
+}
+
+void VolumeCatcherImpl::ChildNode::destroy()
+{
+ if (!mActive) return;
+ mActive = false;
+
+ mImpl->mChildNodesMutex.lock();
+ mImpl->mChildNodes.erase(this);
+ mImpl->mChildNodesMutex.unlock();
+
+ spa_hook_remove(&mNodeListener);
+ spa_hook_remove(&mProxyListener);
+
+ mImpl->pwLock();
+ llpw_proxy_destroy(mProxy);
+ mImpl->pwUnlock();
+}
+
+static pid_t getParentPid(pid_t aPid)
+{
+ std::stringstream strm;
+ strm << "/proc/" << aPid << "/status";
+ std::ifstream in{ strm.str() };
+
+ if (!in.is_open()) return 0;
+
+ pid_t res {0};
+ while (!in.eof() && res == 0)
+ {
+ std::string line;
+ line.resize( 1024, 0 );
+ in.getline( &line[0], line.length() );
+
+ auto i = line.find("PPid:");
+
+ if (i == std::string::npos) continue;
+
+ char const *pIn = line.c_str() + 5; // Skip over pid;
+ while (*pIn != 0 && isspace( *pIn )) ++pIn;
+
+ if (*pIn) res = atoll(pIn);
+ }
+ return res;
+}
+
+static bool isPluginPid(pid_t aPid)
+{
+ auto myPid = getpid();
+
+ do
+ {
+ if(aPid == myPid) return true;
+ aPid = getParentPid( aPid );
+ } while(aPid > 1);
+
+ return false;
+}
+
+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);
+ // debugPrint("got app: %s\n", appName);
+
+ auto* const childNode = static_cast<VolumeCatcherImpl::ChildNode*>(data);
+ // debugPrint("init volume to: %f\n", childNode->mImpl->mDesiredVolume);
+
+ childNode->updateVolume();
+
+ childNode->mImpl->mChildNodesMutex.lock();
+ childNode->mImpl->mChildNodes.insert(childNode);
+ childNode->mImpl->mChildNodesMutex.unlock();
+}
+
+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<VolumeCatcherImpl::ChildNode*>(data);
+ childNode->destroy();
+}
+
+static void proxyEventRemoved(void* data)
+{
+ auto* const childNode = static_cast<VolumeCatcherImpl::ChildNode*>(data);
+ childNode->destroy();
+}
+
+static const struct pw_proxy_events PROXY_EVENTS = {
+ .version = PW_VERSION_PROXY_EVENTS,
+ .destroy = proxyEventDestroy,
+ .removed = proxyEventRemoved,
+};
+
+void VolumeCatcherImpl::handleRegistryEventGlobal(
+ uint32_t id, uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props
+) {
+ if (props == 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 VolumeCatcherImpl::setVolume(F32 volume)
+{
+ // debugPrint("setting all volume to: %f\n", volume);
+
+ mVolume = volume;
+
+ mChildNodesMutex.lock();
+ std::unordered_set<ChildNode*> copyOfChildNodes(mChildNodes);
+ mChildNodesMutex.unlock();
+
+ // debugPrint("for %d nodes\n", copyOfChildNodes.size());
+
+ for (auto* childNode : copyOfChildNodes) {
+ childNode->updateVolume();
+ }
+}
+
+// void VolumeCatcherImpl::setPan(F32 pan)
+// {
+// mPan = pan;
+// setVolume(mVolume);
+// }
+
+/////////////////////////////////////////////////////
+
+VolumeCatcher::VolumeCatcher()
+{
+ pimpl = new VolumeCatcherImpl();
+}
+
+VolumeCatcher::~VolumeCatcher()
+{
+ delete pimpl;
+ pimpl = nullptr;
+}
+
+void VolumeCatcher::setVolume(F32 volume)
+{
+ llassert(pimpl);
+ pimpl->setVolume(volume);
+}
+
+void VolumeCatcher::setPan(F32 pan)
+{
+ // llassert(pimpl);
+ // pimpl->setPan(pan);
+}
+
+void VolumeCatcher::pump()
+{
+}
diff --git a/indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc b/indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc
new file mode 100644
index 0000000000..14e4a42f1d
--- /dev/null
+++ b/indra/media_plugins/cef/linux_volume_catcher_pw_syms.inc
@@ -0,0 +1,22 @@
+// required symbols to grab
+LL_GRAB_SYM(true, pw_init, void, int *argc, char **argv[]);
+// LL_GRAB_SYM(true, pw_main_loop_new, struct pw_main_loop *, const struct spa_dict *props);
+// LL_GRAB_SYM(true, pw_main_loop_get_loop, struct pw_loop *, struct pw_main_loop *loop);
+// LL_GRAB_SYM(true, pw_main_loop_destroy, void, struct pw_main_loop *loop);
+// LL_GRAB_SYM(true, pw_main_loop_run, void, struct pw_main_loop *loop);
+LL_GRAB_SYM(true, pw_context_new, struct pw_context *, struct pw_loop *main_loop, struct pw_properties *props, size_t user_data_size);
+LL_GRAB_SYM(true, pw_context_destroy, void, struct pw_context *context);
+LL_GRAB_SYM(true, pw_context_connect, struct pw_core *, struct pw_context *context, struct pw_properties *properties, size_t user_data_size);
+LL_GRAB_SYM(true, pw_thread_loop_new, struct pw_thread_loop *, const char *name, const struct spa_dict *props);
+LL_GRAB_SYM(true, pw_thread_loop_destroy, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(true, pw_thread_loop_get_loop, struct pw_loop *, struct pw_thread_loop *loop);
+LL_GRAB_SYM(true, pw_thread_loop_start, int, struct pw_thread_loop *loop);
+LL_GRAB_SYM(true, pw_thread_loop_stop, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(true, pw_thread_loop_lock, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(true, pw_thread_loop_unlock, void, struct pw_thread_loop *loop);
+LL_GRAB_SYM(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(true, pw_proxy_destroy, void, struct pw_proxy *proxy);
+LL_GRAB_SYM(true, pw_proxy_get_user_data, void *, struct pw_proxy *proxy);
+LL_GRAB_SYM(true, pw_core_disconnect, int, struct pw_core *core);
+
+// optional symbols to grab