/*** This file is part of PulseAudio. Copyright (C) 2014 Collabora Ltd. PulseAudio 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; either version 2.1 of the License, or (at your option) any later version. PulseAudio 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 General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with PulseAudio; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. ***/ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include PA_MODULE_AUTHOR("Guillaume Desmottes"); PA_MODULE_DESCRIPTION("When a passthrough stream is requested, route all the other streams to a dummy device"); PA_MODULE_VERSION(PACKAGE_VERSION); PA_MODULE_LOAD_ONCE(true); static const char* const valid_modargs[] = { NULL, }; struct userdata { /* (pa_sink *) -> (pa_sink *) * Map the 'real' muted sink to the null-sink currently being used to play * its streams. */ pa_hashmap *null_sinks; bool moving; }; static pa_sink *ensure_null_sink_for_sink(struct userdata *u, pa_sink *s, pa_core *c) { char *t; pa_module *m; pa_sink *sink; uint32_t idx; const char *name; sink = pa_hashmap_get(u->null_sinks, s); if (sink != NULL) { /* We already have a null-sink for this sink */ return sink; } name = pa_proplist_gets(s->proplist, PA_PROP_MEDIA_NAME); t = pa_sprintf_malloc("sink_name=allow_passthrough_null_%s sink_properties='device.description=\"%s\"'", name ? name : "", _("Dummy Output")); pa_module_load(&m, c, "module-null-sink", t); pa_xfree(t); if (m == NULL) return NULL; PA_IDXSET_FOREACH(sink, c->sinks, idx) { if (sink->module->index == m->index) { pa_hashmap_put(u->null_sinks, s, sink); return sink; } } return NULL; } static void unload_null_sink_module_for_sink(struct userdata *u, pa_sink *s, pa_core *c) { pa_sink *null_sink; null_sink = pa_hashmap_get(u->null_sinks, s); if (null_sink == NULL) return; pa_module_unload_request_by_index(c, null_sink->module->index, true); pa_hashmap_remove(u->null_sinks, s); } static void move_stream(struct userdata *u, pa_sink_input *i, pa_sink *target) { u->moving = true; if (pa_sink_input_move_to(i, target, false) < 0) pa_log_info("Failed to move sink input %u \"%s\" to %s.", i->index, pa_strnull(pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_NAME)), target->name); else pa_log_info("Successfully moved sink input %u \"%s\" to %s.", i->index, pa_strnull(pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_NAME)), target->name); u->moving = false; } /* Check if @sink has any passthrough stream, ignoring @ignore */ static bool sink_has_passthrough_stream(pa_sink *sink, pa_sink_input *ignore) { pa_sink_input *stream; uint32_t idx; PA_IDXSET_FOREACH(stream, sink->inputs, idx) { if (stream == ignore) continue; if (pa_sink_input_is_passthrough(stream)) return true; } return false; } static pa_hook_result_t new_passthrough_stream(struct userdata *u, pa_core *c, pa_sink *sink, pa_sink_input *i) { uint32_t idx; pa_sink_input *stream; pa_sink *null_sink; if (sink_has_passthrough_stream(sink, i)) { pa_log_info("Dropping playing a passthrough stream; ignoring"); /* PulseAudio will reject the stream itself */ return PA_HOOK_OK; } pa_log_info("Just received a passthrough stream; pause all the others streams so it can play"); null_sink = ensure_null_sink_for_sink(u, sink, c); if (null_sink == NULL) return PA_HOOK_OK; PA_IDXSET_FOREACH(stream, sink->inputs, idx) { /* We don't want to move the stream which just moved to the sink and trigger this re-routing */ if (stream != i) move_stream(u, stream, null_sink); } return PA_HOOK_OK; } /* return a null sink for the new stream if it needs to be re-routed */ static pa_sink * new_normal_stream(struct userdata *u, pa_core *c, pa_sink *sink) { if (!sink_has_passthrough_stream(sink, NULL)) return NULL; /* A passthrough stream is already playing on this sink, re-route to a null sink */ return ensure_null_sink_for_sink(u, sink, c); } static pa_hook_result_t sink_input_new_cb(pa_core *core, pa_sink_input_new_data *new_data, struct userdata *u) { pa_sink *null_sink; pa_core_assert_ref(core); /* This is a bit of a hack, to determine whether the input stream will use * a passthrough stream, the sink should have been selected and a format * renegotiated. This can either happen by an earlier module (e.g. one * doing routing or other policies) and if not pulseaudio core will setup * the defaults after all hooks for this event have been processed. * * Unfortunately if no other module decides on sink/format before this hook * runs, pulse core doing it is too late, so if a sink and/or stream format * haven't been setup & configured just yet do so now using the same code * as pulsecore would use (default sink and higher priority negotiated * format). */ if (!new_data->sink) { pa_sink *sink = pa_namereg_get(core, NULL, PA_NAMEREG_SINK); pa_return_val_if_fail(sink, PA_HOOK_OK); pa_sink_input_new_data_set_sink(new_data, sink, false, false); } if (!new_data->format && new_data->nego_formats && !pa_idxset_isempty(new_data->nego_formats)) new_data->format = pa_format_info_copy(pa_idxset_first(new_data->nego_formats, NULL)); if (!new_data->format) { /* Sink doesn't support any requested format */ pa_log_debug("Default sink does not match sink input requested formats"); return PA_HOOK_OK; } if (pa_sink_input_new_data_is_passthrough(new_data)) return new_passthrough_stream(u, core, new_data->sink, NULL); null_sink = new_normal_stream(u, core, new_data->sink); if (null_sink) { pa_log_info("Already playing a passthrough stream; re-routing new stream to the null sink"); pa_sink_input_new_data_set_sink(new_data, null_sink, false, false); } return PA_HOOK_OK; } static pa_hook_result_t passthrough_stream_removed(struct userdata *u, pa_core *c, pa_sink_input *i) { uint32_t idx; pa_sink_input *stream; pa_sink *null_sink; pa_assert(i->sink); null_sink = pa_hashmap_get(u->null_sinks, i->sink); if (null_sink == NULL) return PA_HOOK_OK; pa_log_info("Passthrough stream removed; restore all streams"); PA_IDXSET_FOREACH(stream, null_sink->inputs, idx) { move_stream(u, stream, i->sink); } unload_null_sink_module_for_sink(u, i->sink, c); return PA_HOOK_OK; } static pa_hook_result_t sink_input_removed(pa_core *core, pa_sink_input *i, struct userdata *u) { pa_sink_input_assert_ref(i); if (pa_sink_input_is_passthrough(i)) return passthrough_stream_removed(u, core, i); return PA_HOOK_OK; } static pa_hook_result_t sink_input_unlink_cb(pa_core *core, pa_sink_input *i, struct userdata *u) { return sink_input_removed(core, i, u); } static pa_hook_result_t sink_input_move_start_cb(pa_core *core, pa_sink_input *i, struct userdata *u) { if (u->moving) return PA_HOOK_OK; return sink_input_removed(core, i, u); } static pa_hook_result_t sink_input_move_finish_cb(pa_core *core, pa_sink_input *i, struct userdata *u) { pa_sink *null_sink; if (u->moving) return PA_HOOK_OK; if (pa_sink_input_is_passthrough(i)) /* Passthrough stream has been moved to a new sink */ return new_passthrough_stream(u, core, i->sink, i); null_sink = new_normal_stream(u, core, i->sink); if (null_sink) { pa_log_info("Already playing a passthrough stream; re-routing moved stream to the null sink"); move_stream(u, i, null_sink); } return PA_HOOK_OK; } int pa__init(pa_module*m) { pa_modargs *ma; struct userdata *u; pa_assert(m); if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { pa_log("Failed to parse module arguments"); return -1; } m->userdata = u = pa_xnew(struct userdata, 1); u->null_sinks = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_INPUT_NEW], PA_HOOK_LATE, (pa_hook_cb_t) sink_input_new_cb, u); pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_INPUT_UNLINK], PA_HOOK_LATE, (pa_hook_cb_t) sink_input_unlink_cb, u); pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_INPUT_MOVE_START], PA_HOOK_LATE, (pa_hook_cb_t) sink_input_move_start_cb, u); pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_INPUT_MOVE_FINISH], PA_HOOK_LATE, (pa_hook_cb_t) sink_input_move_finish_cb, u); u->moving = false; pa_modargs_free(ma); return 0; } static void unload_all_null_sink_modules(struct userdata *u, pa_core *c) { void *state = NULL; pa_sink *null_sink; PA_HASHMAP_FOREACH(null_sink, u->null_sinks, state) pa_module_unload_request_by_index(c, null_sink->module->index, true); } void pa__done(pa_module*m) { struct userdata *u; pa_assert(m); if (!(u = m->userdata)) return; if (m->core->state != PA_CORE_SHUTDOWN) unload_all_null_sink_modules(u, m->core); if (u->null_sinks) pa_hashmap_free(u->null_sinks); pa_xfree(u); }