diff options
Diffstat (limited to 'indra/media_plugins/cef/linux')
7 files changed, 965 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..86cc329e66 --- /dev/null +++ b/indra/media_plugins/cef/linux/volume_catcher_linux.cpp @@ -0,0 +1,103 @@ +/** + * @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" + +#define PULSEAUDIO pimpl +#define PIPEWIRE pimpl2 + +//////////////////////////////////////////////////// + +VolumeCatcher::VolumeCatcher() +{ + // only init once we receive which implementation to use + + // debugClear(); + // debugPrint("init volume catcher\n"); +} + +void VolumeCatcher::onEnablePipeWireVolumeCatcher(bool enable) { + if (enable) { + // load pipewire + + if (PULSEAUDIO != nullptr) { + delete PULSEAUDIO; + PULSEAUDIO = nullptr; + } + + if (PIPEWIRE == nullptr) { + // debugPrint("volume catcher using pipewire\n"); + PIPEWIRE = new VolumeCatcherPipeWire(); + } + } else { + // load pulseaudio + + if (PIPEWIRE != nullptr) { + delete PIPEWIRE; + PIPEWIRE = nullptr; + } + + if (PULSEAUDIO == nullptr) { + // debugPrint("volume catcher using pulseaudio\n"); + PULSEAUDIO = new VolumeCatcherPulseAudio(); + } + } +} + +VolumeCatcher::~VolumeCatcher() +{ + if (PULSEAUDIO != nullptr) { + delete PULSEAUDIO; + PULSEAUDIO = nullptr; + } + + if (PIPEWIRE != nullptr) { + delete PIPEWIRE; + PIPEWIRE = nullptr; + } +} + +void VolumeCatcher::setVolume(F32 volume) +{ + if (PULSEAUDIO != nullptr) PULSEAUDIO->setVolume(volume); + if (PIPEWIRE != nullptr) PIPEWIRE->setVolume(volume); +} + +void VolumeCatcher::setPan(F32 pan) +{ + // not implemented for both + // if (PULSEAUDIO) PULSEAUDIO->setPan(pan); + // if (PIPEWIRE) PIPEWIRE->setPan(pan); +} + +void VolumeCatcher::pump() +{ + if (PULSEAUDIO != nullptr) PULSEAUDIO->pump(); + if (PIPEWIRE != nullptr) PIPEWIRE->pump(); // doesn't need it +} + 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..5149dd62e0 --- /dev/null +++ b/indra/media_plugins/cef/linux/volume_catcher_linux.h @@ -0,0 +1,151 @@ +/** + * @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 VolumeCatcherPulseAudio : public virtual 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 virtual VolumeCatcherImpl +{ +public: + VolumeCatcherPipeWire(); + ~VolumeCatcherPipeWire(); + + bool loadsyms(std::string pw_dso_name); + void init(); + void cleanup(); + + // some of these should be private + + void pwLock(); + void pwUnlock(); + + 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; + pw_context* mContext; + pw_core* mCore; + pw_registry* mRegistry; + spa_hook mRegistryListener; + + std::unordered_set<ChildNode*> mChildNodes; + std::mutex mChildNodesMutex; +}; + +// static void debugClear() +// { +// auto file = fopen("volume-catcher-log.txt", "w"); +// fprintf(file, "\n"); +// fclose(file); +// } + +// static void debugPrint(const char* format, ...) +// { +// va_list args; +// va_start(args, format); +// auto file = fopen("volume-catcher-log.txt", "a"); +// vfprintf(file, format, args); +// fclose(file); +// va_end(args); +// } + +#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..a779cf868e --- /dev/null +++ b/indra/media_plugins/cef/linux/volume_catcher_pipewire.cpp @@ -0,0 +1,336 @@ +/** + * @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; +#undef LL_SYMBOL_GRABBER +#define LL_SYMBOL_GRABBER 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() +{ + // debugPrint("init\n"); + + mGotSyms = loadsyms("libpipewire-0.3.so.0"); + + if (!mGotSyms) + return; + + // debugPrint("got syms\n"); + + 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, ®ISTRY_EVENTS, this + ); + + llpw_thread_loop_start(mThreadLoop); + + pwUnlock(); + + // debugPrint("started thread loop\n"); +} + +void VolumeCatcherPipeWire::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 VolumeCatcherPipeWire::pwLock() { + if (!mThreadLoop) + return; + + llpw_thread_loop_lock(mThreadLoop); +} + +void VolumeCatcherPipeWire::pwUnlock() { + if (!mThreadLoop) + return; + + llpw_thread_loop_unlock(mThreadLoop); +} + +// #define INV_LERP(a, b, v) (v - a) / (b - a) + +// #include <sys/time.h> + +void VolumeCatcherPipeWire::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 VolumeCatcherPipeWire::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 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<VolumeCatcherPipeWire::ChildNode*>(data); + // debugPrint("init volume to: %f\n", childNode->mImpl->mVolume); + + 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<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 || 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) +{ + // 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 VolumeCatcherPipeWire::setPan(F32 pan) +{ + // mPan = pan; + // setVolume(mVolume); +} + +void VolumeCatcherPipeWire::pump() +{ +}
\ No newline at end of file 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..14e4a42f1d --- /dev/null +++ b/indra/media_plugins/cef/linux/volume_catcher_pipewire_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 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..20a6458ea3 --- /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; +#undef LL_SYMBOL_GRABBER +#define LL_SYMBOL_GRABBER 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..5fba60c188 --- /dev/null +++ b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_glib_syms.inc @@ -0,0 +1,6 @@ +// required symbols to grab +LL_GRAB_SYM(true, pa_glib_mainloop_free, void, pa_glib_mainloop* g) +LL_GRAB_SYM(true, pa_glib_mainloop_get_api, pa_mainloop_api*, pa_glib_mainloop* g) +LL_GRAB_SYM(true, pa_glib_mainloop_new, pa_glib_mainloop *, GMainContext *c) + +// optional symbols to grab 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..fe4ad084b6 --- /dev/null +++ b/indra/media_plugins/cef/linux/volume_catcher_pulseaudio_syms.inc @@ -0,0 +1,25 @@ +// required symbols to grab +LL_GRAB_SYM(true, pa_context_connect, int, pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api) +LL_GRAB_SYM(true, pa_context_disconnect, void, pa_context *c) +LL_GRAB_SYM(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(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(true, pa_context_get_state, pa_context_state_t, pa_context *c) +LL_GRAB_SYM(true, pa_context_new_with_proplist, pa_context*, pa_mainloop_api *mainloop, const char *name, pa_proplist *proplist) +LL_GRAB_SYM(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(true, pa_context_set_state_callback, void, pa_context *c, pa_context_notify_cb_t cb, void *userdata) +LL_GRAB_SYM(true, pa_context_set_subscribe_callback, void, pa_context *c, pa_context_subscribe_cb_t cb, void *userdata) +LL_GRAB_SYM(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(true, pa_context_unref, void, pa_context *c) +LL_GRAB_SYM(true, pa_cvolume_set, pa_cvolume*, pa_cvolume *a, unsigned channels, pa_volume_t v) +LL_GRAB_SYM(true, pa_operation_unref, void, pa_operation *o) +LL_GRAB_SYM(true, pa_proplist_free, void, pa_proplist* p) +LL_GRAB_SYM(true, pa_proplist_gets, const char*, pa_proplist *p, const char *key) +LL_GRAB_SYM(true, pa_proplist_new, pa_proplist*, void) +LL_GRAB_SYM(true, pa_proplist_sets, int, pa_proplist *p, const char *key, const char *value) +LL_GRAB_SYM(true, pa_sw_volume_from_linear, pa_volume_t, double v) +// LL_GRAB_SYM(true, pa_mainloop_free, void, pa_mainloop *m) +// LL_GRAB_SYM(true, pa_mainloop_get_api, pa_mainloop_api *, pa_mainloop *m) +// LL_GRAB_SYM(true, pa_mainloop_iterate, int, pa_mainloop *m, int block, int *retval) +// LL_GRAB_SYM(true, pa_mainloop_new, pa_mainloop *, void) + +// optional symbols to grab |