PageRenderTime 456ms CodeModel.GetById 141ms app.highlight 183ms RepoModel.GetById 126ms app.codeStats 0ms

/indra/media_plugins/webkit/linux_volume_catcher.cpp

https://bitbucket.org/lindenlab/viewer-beta/
C++ | 468 lines | 324 code | 76 blank | 68 comment | 41 complexity | 3c7997d17c36fd8976fd4553cfaf7b61 MD5 | raw file
  1/** 
  2 * @file linux_volume_catcher.cpp
  3 * @brief A Linux-specific, PulseAudio-specific hack to detect and volume-adjust new audio sources
  4 *
  5 * @cond
  6 * $LicenseInfo:firstyear=2010&license=viewerlgpl$
  7 * Second Life Viewer Source Code
  8 * Copyright (C) 2010, Linden Research, Inc.
  9 * 
 10 * This library is free software; you can redistribute it and/or
 11 * modify it under the terms of the GNU Lesser General Public
 12 * License as published by the Free Software Foundation;
 13 * version 2.1 of the License only.
 14 * 
 15 * This library is distributed in the hope that it will be useful,
 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 18 * Lesser General Public License for more details.
 19 * 
 20 * You should have received a copy of the GNU Lesser General Public
 21 * License along with this library; if not, write to the Free Software
 22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 23 * 
 24 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 25 * $/LicenseInfo$
 26 * @endcond
 27 */
 28
 29/*
 30  The high-level design is as follows:
 31  1) Connect to the PulseAudio daemon
 32  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)
 33  3) Examine any new audio player's PID to see if it belongs to our own process
 34  4) If so, tell PA to adjust the volume of that audio player ('sink input' in PA parlance)
 35  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
 36 */
 37
 38#include "linden_common.h"
 39
 40#include "volume_catcher.h"
 41
 42
 43extern "C" {
 44#include <glib.h>
 45#include <glib-object.h>
 46
 47#include <pulse/introspect.h>
 48#include <pulse/context.h>
 49#include <pulse/subscribe.h>
 50#include <pulse/glib-mainloop.h> // There's no special reason why we want the *glib* PA mainloop, but the generic polling implementation seems broken.
 51
 52#include "apr_pools.h"
 53#include "apr_dso.h"
 54}
 55
 56////////////////////////////////////////////////////
 57
 58#define DEBUGMSG(...) do {} while(0)
 59#define INFOMSG(...) do {} while(0)
 60#define WARNMSG(...) do {} while(0)
 61
 62#define LL_PA_SYM(REQUIRED, PASYM, RTN, ...) RTN (*ll##PASYM)(__VA_ARGS__) = NULL
 63#include "linux_volume_catcher_pa_syms.inc"
 64#include "linux_volume_catcher_paglib_syms.inc"
 65#undef LL_PA_SYM
 66
 67static bool sSymsGrabbed = false;
 68static apr_pool_t *sSymPADSOMemoryPool = NULL;
 69static apr_dso_handle_t *sSymPADSOHandleG = NULL;
 70
 71bool grab_pa_syms(std::string pulse_dso_name)
 72{
 73	if (sSymsGrabbed)
 74	{
 75		// already have grabbed good syms
 76		return true;
 77	}
 78
 79	bool sym_error = false;
 80	bool rtn = false;
 81	apr_status_t rv;
 82	apr_dso_handle_t *sSymPADSOHandle = NULL;
 83
 84#define LL_PA_SYM(REQUIRED, PASYM, RTN, ...) do{rv = apr_dso_sym((apr_dso_handle_sym_t*)&ll##PASYM, sSymPADSOHandle, #PASYM); if (rv != APR_SUCCESS) {INFOMSG("Failed to grab symbol: %s", #PASYM); if (REQUIRED) sym_error = true;} else DEBUGMSG("grabbed symbol: %s from %p", #PASYM, (void*)ll##PASYM);}while(0)
 85
 86	//attempt to load the shared library
 87	apr_pool_create(&sSymPADSOMemoryPool, NULL);
 88  
 89	if ( APR_SUCCESS == (rv = apr_dso_load(&sSymPADSOHandle,
 90					       pulse_dso_name.c_str(),
 91					       sSymPADSOMemoryPool) ))
 92	{
 93		INFOMSG("Found DSO: %s", pulse_dso_name.c_str());
 94
 95#include "linux_volume_catcher_pa_syms.inc"
 96#include "linux_volume_catcher_paglib_syms.inc"
 97      
 98		if ( sSymPADSOHandle )
 99		{
100			sSymPADSOHandleG = sSymPADSOHandle;
101			sSymPADSOHandle = NULL;
102		}
103      
104		rtn = !sym_error;
105	}
106	else
107	{
108		INFOMSG("Couldn't load DSO: %s", pulse_dso_name.c_str());
109		rtn = false; // failure
110	}
111
112	if (sym_error)
113	{
114		WARNMSG("Failed to find necessary symbols in PulseAudio libraries.");
115	}
116#undef LL_PA_SYM
117
118	sSymsGrabbed = rtn;
119	return rtn;
120}
121
122
123void ungrab_pa_syms()
124{ 
125	// should be safe to call regardless of whether we've
126	// actually grabbed syms.
127
128	if ( sSymPADSOHandleG )
129	{
130		apr_dso_unload(sSymPADSOHandleG);
131		sSymPADSOHandleG = NULL;
132	}
133	
134	if ( sSymPADSOMemoryPool )
135	{
136		apr_pool_destroy(sSymPADSOMemoryPool);
137		sSymPADSOMemoryPool = NULL;
138	}
139	
140	// NULL-out all of the symbols we'd grabbed
141#define LL_PA_SYM(REQUIRED, PASYM, RTN, ...) do{ll##PASYM = NULL;}while(0)
142#include "linux_volume_catcher_pa_syms.inc"
143#include "linux_volume_catcher_paglib_syms.inc"
144#undef LL_PA_SYM
145
146	sSymsGrabbed = false;
147}
148////////////////////////////////////////////////////
149
150// PulseAudio requires a chain of callbacks with C linkage
151extern "C" {
152	void callback_discovered_sinkinput(pa_context *context, const pa_sink_input_info *i, int eol, void *userdata);
153	void callback_subscription_alert(pa_context *context, pa_subscription_event_type_t t, uint32_t index, void *userdata);
154	void callback_context_state(pa_context *context, void *userdata);
155}
156
157
158class VolumeCatcherImpl
159{
160public:
161	VolumeCatcherImpl();
162	~VolumeCatcherImpl();
163
164	void setVolume(F32 volume);
165	void pump(void);
166
167	// for internal use - can't be private because used from our C callbacks
168
169	bool loadsyms(std::string pulse_dso_name);
170	void init();
171	void cleanup();
172
173	void update_all_volumes(F32 volume);
174	void update_index_volume(U32 index, F32 volume);
175	void connected_okay();
176
177	std::set<U32> mSinkInputIndices;
178	std::map<U32,U32> mSinkInputNumChannels;
179	F32 mDesiredVolume;
180	pa_glib_mainloop *mMainloop;
181	pa_context *mPAContext;
182	bool mConnected;
183	bool mGotSyms;
184};
185
186VolumeCatcherImpl::VolumeCatcherImpl()
187	: mDesiredVolume(0.0f),
188	  mMainloop(NULL),
189	  mPAContext(NULL),
190	  mConnected(false),
191	  mGotSyms(false)
192{
193	init();
194}
195
196VolumeCatcherImpl::~VolumeCatcherImpl()
197{
198	cleanup();
199}
200
201bool VolumeCatcherImpl::loadsyms(std::string pulse_dso_name)
202{
203	return grab_pa_syms(pulse_dso_name);
204}
205
206void VolumeCatcherImpl::init()
207{
208	// try to be as defensive as possible because PA's interface is a
209	// bit fragile and (for our purposes) we'd rather simply not function
210	// than crash
211
212	// we cheat and rely upon libpulse-mainloop-glib.so.0 to pull-in
213	// libpulse.so.0 - this isn't a great assumption, and the two DSOs should
214	// probably be loaded separately.  Our Linux DSO framework needs refactoring,
215	// we do this sort of thing a lot with practically identical logic...
216	mGotSyms = loadsyms("libpulse-mainloop-glib.so.0");
217	if (!mGotSyms) return;
218
219	// better make double-sure glib itself is initialized properly.
220	if (!g_thread_supported ()) g_thread_init (NULL);
221	g_type_init();
222
223	mMainloop = llpa_glib_mainloop_new(g_main_context_default());
224	if (mMainloop)
225	{
226		pa_mainloop_api *api = llpa_glib_mainloop_get_api(mMainloop);
227		if (api)
228		{
229			pa_proplist *proplist = llpa_proplist_new();
230			if (proplist)
231			{
232				llpa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "multimedia-player");
233				llpa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "com.secondlife.viewer.mediaplugvoladjust");
234				llpa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "SL Plugin Volume Adjuster");
235				llpa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, "1");
236
237				// plain old pa_context_new() is broken!
238				mPAContext = llpa_context_new_with_proplist(api, NULL, proplist);
239				llpa_proplist_free(proplist);
240			}
241		}
242	}
243
244	// Now we've set up a PA context and mainloop, try connecting the
245	// PA context to a PA daemon.
246	if (mPAContext)
247	{
248		llpa_context_set_state_callback(mPAContext, callback_context_state, this);
249		pa_context_flags_t cflags = (pa_context_flags)0; // maybe add PA_CONTEXT_NOAUTOSPAWN?
250		if (llpa_context_connect(mPAContext, NULL, cflags, NULL) >= 0)
251		{
252			// Okay!  We haven't definitely connected, but we
253			// haven't definitely failed yet.
254		}
255		else
256		{
257			// Failed to connect to PA manager... we'll leave
258			// things like that.  Perhaps we should try again later.
259		}
260	}
261}
262
263void VolumeCatcherImpl::cleanup()
264{
265	mConnected = false;
266
267	if (mGotSyms && mPAContext)
268	{
269		llpa_context_disconnect(mPAContext);
270		llpa_context_unref(mPAContext);
271	}
272	mPAContext = NULL;
273
274	if (mGotSyms && mMainloop)
275	{
276		llpa_glib_mainloop_free(mMainloop);
277	}
278	mMainloop = NULL;
279}
280
281void VolumeCatcherImpl::setVolume(F32 volume)
282{
283	mDesiredVolume = volume;
284	
285	if (!mGotSyms) return;
286
287	if (mConnected && mPAContext)
288	{
289		update_all_volumes(mDesiredVolume);
290	}
291
292	pump();
293}
294
295void VolumeCatcherImpl::pump()
296{
297	gboolean may_block = FALSE;
298	g_main_context_iteration(g_main_context_default(), may_block);
299}
300
301void VolumeCatcherImpl::connected_okay()
302{
303	pa_operation *op;
304
305	// fetch global list of existing sinkinputs
306	if ((op = llpa_context_get_sink_input_info_list(mPAContext,
307							callback_discovered_sinkinput,
308							this)))
309	{
310		llpa_operation_unref(op);
311	}
312
313	// subscribe to future global sinkinput changes
314	llpa_context_set_subscribe_callback(mPAContext,
315					    callback_subscription_alert,
316					    this);
317	if ((op = llpa_context_subscribe(mPAContext, (pa_subscription_mask_t)
318					 (PA_SUBSCRIPTION_MASK_SINK_INPUT),
319					 NULL, NULL)))
320	{
321		llpa_operation_unref(op);
322	}
323}
324
325void VolumeCatcherImpl::update_all_volumes(F32 volume)
326{
327	for (std::set<U32>::iterator it = mSinkInputIndices.begin();
328	     it != mSinkInputIndices.end(); ++it)
329	{
330		update_index_volume(*it, volume);
331	}
332}
333
334void VolumeCatcherImpl::update_index_volume(U32 index, F32 volume)
335{
336	static pa_cvolume cvol;
337	llpa_cvolume_set(&cvol, mSinkInputNumChannels[index],
338			 llpa_sw_volume_from_linear(volume));
339	
340	pa_context *c = mPAContext;
341	uint32_t idx = index;
342	const pa_cvolume *cvolumep = &cvol;
343	pa_context_success_cb_t cb = NULL; // okay as null
344	void *userdata = NULL; // okay as null
345
346	pa_operation *op;
347	if ((op = llpa_context_set_sink_input_volume(c, idx, cvolumep, cb, userdata)))
348	{
349		llpa_operation_unref(op);
350	}
351}
352
353
354void callback_discovered_sinkinput(pa_context *context, const pa_sink_input_info *sii, int eol, void *userdata)
355{
356	VolumeCatcherImpl *impl = dynamic_cast<VolumeCatcherImpl*>((VolumeCatcherImpl*)userdata);
357	llassert(impl);
358
359	if (0 == eol)
360	{
361		pa_proplist *proplist = sii->proplist;
362		pid_t sinkpid = atoll(llpa_proplist_gets(proplist, PA_PROP_APPLICATION_PROCESS_ID));
363		
364		if (sinkpid == getpid()) // does the discovered sinkinput belong to this process?
365		{
366			bool is_new = (impl->mSinkInputIndices.find(sii->index) ==
367				       impl->mSinkInputIndices.end());
368			
369			impl->mSinkInputIndices.insert(sii->index);
370			impl->mSinkInputNumChannels[sii->index] = sii->channel_map.channels;
371			
372			if (is_new)
373			{
374				// new!
375				impl->update_index_volume(sii->index, impl->mDesiredVolume);
376			}
377			else
378			{
379				// seen it already, do nothing.
380			}
381		}
382	}
383}
384
385void callback_subscription_alert(pa_context *context, pa_subscription_event_type_t t, uint32_t index, void *userdata)
386{
387	VolumeCatcherImpl *impl = dynamic_cast<VolumeCatcherImpl*>((VolumeCatcherImpl*)userdata);
388	llassert(impl);
389
390	switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
391        case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
392		if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) ==
393		    PA_SUBSCRIPTION_EVENT_REMOVE)
394		{
395			// forget this sinkinput, if we were caring about it
396			impl->mSinkInputIndices.erase(index);
397			impl->mSinkInputNumChannels.erase(index);
398		}
399		else if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) ==
400			 PA_SUBSCRIPTION_EVENT_NEW)
401		{
402			// ask for more info about this new sinkinput
403			pa_operation *op;
404			if ((op = llpa_context_get_sink_input_info(impl->mPAContext, index, callback_discovered_sinkinput, impl)))
405			{
406				llpa_operation_unref(op);
407			}
408		}
409		else
410		{
411			// property change on this sinkinput - we don't care.
412		}
413		break;
414		
415	default:;
416	}
417}
418
419void callback_context_state(pa_context *context, void *userdata)
420{
421	VolumeCatcherImpl *impl = dynamic_cast<VolumeCatcherImpl*>((VolumeCatcherImpl*)userdata);
422	llassert(impl);
423	
424	switch (llpa_context_get_state(context))
425	{
426	case PA_CONTEXT_READY:
427		impl->mConnected = true;
428		impl->connected_okay();
429		break;
430	case PA_CONTEXT_TERMINATED:
431		impl->mConnected = false;
432		break;
433	case PA_CONTEXT_FAILED:
434		impl->mConnected = false;
435		break;
436	default:;
437	}
438}
439
440/////////////////////////////////////////////////////
441
442VolumeCatcher::VolumeCatcher()
443{
444	pimpl = new VolumeCatcherImpl();
445}
446
447VolumeCatcher::~VolumeCatcher()
448{
449	delete pimpl;
450	pimpl = NULL;
451}
452
453void VolumeCatcher::setVolume(F32 volume)
454{
455	llassert(pimpl);
456	pimpl->setVolume(volume);
457}
458
459void VolumeCatcher::setPan(F32 pan)
460{
461	// TODO: implement this (if possible)
462}
463
464void VolumeCatcher::pump()
465{
466	llassert(pimpl);
467	pimpl->pump();
468}