/*** This file is part of PulseAudio. Copyright 2006-2008 Lennart Poettering Copyright 2009 Colin Guthrie 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "module-device-manager-symdef.h" PA_MODULE_AUTHOR("Colin Guthrie"); PA_MODULE_DESCRIPTION("Keep track of devices (and their descriptions) both past and present and prioritise by role"); PA_MODULE_VERSION(PACKAGE_VERSION); PA_MODULE_LOAD_ONCE(TRUE); PA_MODULE_USAGE( "do_routing= " "on_hotplug= " "on_rescue="); #define SAVE_INTERVAL (10 * PA_USEC_PER_SEC) #define DUMP_DATABASE static const char* const valid_modargs[] = { "do_routing", "on_hotplug", "on_rescue", NULL }; #define NUM_ROLES 9 enum { ROLE_NONE, ROLE_VIDEO, ROLE_MUSIC, ROLE_GAME, ROLE_EVENT, ROLE_PHONE, ROLE_ANIMATION, ROLE_PRODUCTION, ROLE_A11Y, }; typedef uint32_t role_indexes_t[NUM_ROLES]; static const char* role_names[NUM_ROLES] = { "none", "video", "music", "game", "event", "phone", "animation", "production", "a11y", }; struct userdata { pa_core *core; pa_module *module; pa_subscription *subscription; pa_hook_slot *sink_new_hook_slot, *source_new_hook_slot, *sink_input_new_hook_slot, *source_output_new_hook_slot, *sink_put_hook_slot, *source_put_hook_slot, *sink_unlink_hook_slot, *source_unlink_hook_slot, *connection_unlink_hook_slot; pa_time_event *save_time_event; pa_database *database; pa_native_protocol *protocol; pa_idxset *subscribed; pa_bool_t on_hotplug; pa_bool_t on_rescue; pa_bool_t do_routing; role_indexes_t preferred_sinks; role_indexes_t preferred_sources; }; #define ENTRY_VERSION 1 struct entry { uint8_t version; char description[PA_NAME_MAX]; pa_bool_t user_set_description; char icon[PA_NAME_MAX]; role_indexes_t priority; } PA_GCC_PACKED; enum { SUBCOMMAND_TEST, SUBCOMMAND_READ, SUBCOMMAND_RENAME, SUBCOMMAND_DELETE, SUBCOMMAND_ROLE_DEVICE_PRIORITY_ROUTING, SUBCOMMAND_REORDER, SUBCOMMAND_SUBSCRIBE, SUBCOMMAND_EVENT }; static struct entry* read_entry(struct userdata *u, const char *name) { pa_datum key, data; struct entry *e; pa_assert(u); pa_assert(name); key.data = (char*) name; key.size = strlen(name); pa_zero(data); if (!pa_database_get(u->database, &key, &data)) goto fail; if (data.size != sizeof(struct entry)) { pa_log_debug("Database contains entry for device %s of wrong size %lu != %lu. Probably due to upgrade, ignoring.", name, (unsigned long) data.size, (unsigned long) sizeof(struct entry)); goto fail; } e = (struct entry*) data.data; if (e->version != ENTRY_VERSION) { pa_log_debug("Version of database entry for device %s doesn't match our version. Probably due to upgrade, ignoring.", name); goto fail; } if (!memchr(e->description, 0, sizeof(e->description))) { pa_log_warn("Database contains entry for device %s with missing NUL byte in description", name); goto fail; } if (!memchr(e->icon, 0, sizeof(e->icon))) { pa_log_warn("Database contains entry for device %s with missing NUL byte in icon", name); goto fail; } return e; fail: pa_datum_free(&data); return NULL; } #ifdef DUMP_DATABASE static void dump_database_helper(struct userdata *u, uint32_t role_index, const char* human, pa_bool_t sink_mode) { pa_assert(u); pa_assert(human); if (sink_mode) { pa_sink *s; if (PA_INVALID_INDEX != u->preferred_sinks[role_index] && (s = pa_idxset_get_by_index(u->core->sinks, u->preferred_sinks[role_index]))) pa_log_debug(" %s %s (%s)", human, pa_strnull(pa_proplist_gets(s->proplist, PA_PROP_DEVICE_DESCRIPTION)), s->name); else pa_log_debug(" %s No sink specified", human); } else { pa_source *s; if (PA_INVALID_INDEX != u->preferred_sources[role_index] && (s = pa_idxset_get_by_index(u->core->sources, u->preferred_sources[role_index]))) pa_log_debug(" %s %s (%s)", human, pa_strnull(pa_proplist_gets(s->proplist, PA_PROP_DEVICE_DESCRIPTION)), s->name); else pa_log_debug(" %s No source specified", human); } } static void dump_database(struct userdata *u) { pa_datum key; pa_bool_t done; pa_assert(u); done = !pa_database_first(u->database, &key, NULL); pa_log_debug("Dumping database"); while (!done) { char *name; struct entry *e; pa_datum next_key; done = !pa_database_next(u->database, &key, &next_key, NULL); name = pa_xstrndup(key.data, key.size); if ((e = read_entry(u, name))) { pa_log_debug(" Got entry: %s", name); pa_log_debug(" Description: %s", e->description); pa_log_debug(" Priorities: None: %3u, Video: %3u, Music: %3u, Game: %3u, Event: %3u", e->priority[ROLE_NONE], e->priority[ROLE_VIDEO], e->priority[ROLE_MUSIC], e->priority[ROLE_GAME], e->priority[ROLE_EVENT]); pa_log_debug(" Phone: %3u, Anim: %3u, Prodtn: %3u, A11y: %3u", e->priority[ROLE_PHONE], e->priority[ROLE_ANIMATION], e->priority[ROLE_PRODUCTION], e->priority[ROLE_A11Y]); pa_xfree(e); } pa_xfree(name); pa_datum_free(&key); key = next_key; } if (u->do_routing) { pa_log_debug(" Highest priority devices per-role:"); pa_log_debug(" Sinks:"); for (uint32_t role = ROLE_NONE; role < NUM_ROLES; ++role) { char name[13]; uint32_t len = PA_MIN(12u, strlen(role_names[role])); strncpy(name, role_names[role], len); for (int i = len+1; i < 12; ++i) name[i] = ' '; name[len] = ':'; name[0] -= 32; name[12] = '\0'; dump_database_helper(u, role, name, TRUE); } pa_log_debug(" Sources:"); for (uint32_t role = ROLE_NONE; role < NUM_ROLES; ++role) { char name[13]; uint32_t len = PA_MIN(12u, strlen(role_names[role])); strncpy(name, role_names[role], len); for (int i = len+1; i < 12; ++i) name[i] = ' '; name[len] = ':'; name[0] -= 32; name[12] = '\0'; dump_database_helper(u, role, name, FALSE); } } pa_log_debug("Completed database dump"); } #endif static void save_time_callback(pa_mainloop_api*a, pa_time_event* e, const struct timeval *t, void *userdata) { struct userdata *u = userdata; pa_assert(a); pa_assert(e); pa_assert(u); pa_assert(e == u->save_time_event); u->core->mainloop->time_free(u->save_time_event); u->save_time_event = NULL; pa_database_sync(u->database); pa_log_info("Synced."); #ifdef DUMP_DATABASE dump_database(u); #endif } static void notify_subscribers(struct userdata *u) { pa_native_connection *c; uint32_t idx; pa_assert(u); for (c = pa_idxset_first(u->subscribed, &idx); c; c = pa_idxset_next(u->subscribed, &idx)) { pa_tagstruct *t; t = pa_tagstruct_new(NULL, 0); pa_tagstruct_putu32(t, PA_COMMAND_EXTENSION); pa_tagstruct_putu32(t, 0); pa_tagstruct_putu32(t, u->module->index); pa_tagstruct_puts(t, u->module->name); pa_tagstruct_putu32(t, SUBCOMMAND_EVENT); pa_pstream_send_tagstruct(pa_native_connection_get_pstream(c), t); } } static void trigger_save(struct userdata *u) { pa_assert(u); notify_subscribers(u); if (u->save_time_event) return; u->save_time_event = pa_core_rttime_new(u->core, pa_rtclock_now() + SAVE_INTERVAL, save_time_callback, u); } static pa_bool_t entries_equal(const struct entry *a, const struct entry *b) { pa_assert(a); pa_assert(b); if (strncmp(a->description, b->description, sizeof(a->description)) || a->user_set_description != b->user_set_description || strncmp(a->icon, b->icon, sizeof(a->icon))) return FALSE; for (int i=0; i < NUM_ROLES; ++i) if (a->priority[i] != b->priority[i]) return FALSE; return TRUE; } static char *get_name(const char *key, const char *prefix) { char *t; if (strncmp(key, prefix, strlen(prefix))) return NULL; t = pa_xstrdup(key + strlen(prefix)); return t; } static inline struct entry *load_or_initialize_entry(struct userdata *u, struct entry *entry, const char *name, const char *prefix) { struct entry *old; pa_assert(u); pa_assert(entry); pa_assert(name); pa_assert(prefix); if ((old = read_entry(u, name))) *entry = *old; else { /* This is a new device, so make sure we write it's priority list correctly */ role_indexes_t max_priority; pa_datum key; pa_bool_t done; pa_zero(max_priority); done = !pa_database_first(u->database, &key, NULL); /* Find all existing devices with the same prefix so we calculate the current max priority for each role */ while (!done) { pa_datum next_key; done = !pa_database_next(u->database, &key, &next_key, NULL); if (key.size > strlen(prefix) && strncmp(key.data, prefix, strlen(prefix)) == 0) { char *name2; struct entry *e; name2 = pa_xstrndup(key.data, key.size); if ((e = read_entry(u, name2))) { for (uint32_t i = 0; i < NUM_ROLES; ++i) { max_priority[i] = PA_MAX(max_priority[i], e->priority[i]); } pa_xfree(e); } pa_xfree(name2); } pa_datum_free(&key); key = next_key; } /* Actually initialise our entry now we've calculated it */ for (uint32_t i = 0; i < NUM_ROLES; ++i) { entry->priority[i] = max_priority[i] + 1; } entry->user_set_description = FALSE; } return old; } static uint32_t get_role_index(const char* role) { pa_assert(role); for (uint32_t i = ROLE_NONE; i < NUM_ROLES; ++i) if (strcmp(role, role_names[i]) == 0) return i; return PA_INVALID_INDEX; } static void update_highest_priority_device_indexes(struct userdata *u, const char *prefix, void *ignore_device) { role_indexes_t *indexes, highest_priority_available; pa_datum key; pa_bool_t done, sink_mode; pa_assert(u); pa_assert(prefix); sink_mode = (strcmp(prefix, "sink:") == 0); if (sink_mode) indexes = &u->preferred_sinks; else indexes = &u->preferred_sources; for (uint32_t i = 0; i < NUM_ROLES; ++i) { (*indexes)[i] = PA_INVALID_INDEX; } pa_zero(highest_priority_available); done = !pa_database_first(u->database, &key, NULL); /* Find all existing devices with the same prefix so we find the highest priority device for each role */ while (!done) { pa_datum next_key; done = !pa_database_next(u->database, &key, &next_key, NULL); if (key.size > strlen(prefix) && strncmp(key.data, prefix, strlen(prefix)) == 0) { char *name, *device_name; struct entry *e; name = pa_xstrndup(key.data, key.size); device_name = get_name(name, prefix); if ((e = read_entry(u, name))) { for (uint32_t i = 0; i < NUM_ROLES; ++i) { if (!highest_priority_available[i] || e->priority[i] < highest_priority_available[i]) { /* We've found a device with a higher priority than that we've currently got, so see if it is currently available or not and update our list */ uint32_t idx; pa_bool_t found = FALSE; if (sink_mode) { pa_sink *sink; PA_IDXSET_FOREACH(sink, u->core->sinks, idx) { if ((pa_sink*) ignore_device == sink) continue; if (strcmp(sink->name, device_name) == 0) { found = TRUE; idx = sink->index; /* Is this needed? */ break; } } } else { pa_source *source; PA_IDXSET_FOREACH(source, u->core->sources, idx) { if ((pa_source*) ignore_device == source) continue; if (strcmp(source->name, device_name) == 0) { found = TRUE; idx = source->index; /* Is this needed? */ break; } } } if (found) { highest_priority_available[i] = e->priority[i]; (*indexes)[i] = idx; } } } pa_xfree(e); } pa_xfree(name); pa_xfree(device_name); } pa_datum_free(&key); key = next_key; } } static void route_sink_input(struct userdata *u, pa_sink_input *si) { const char *role; uint32_t role_index, device_index; pa_sink *sink; pa_assert(u); pa_assert(u->do_routing); if (si->save_sink) return; /* Skip this if it is already in the process of being moved anyway */ if (!si->sink) return; /* It might happen that a stream and a sink are set up at the same time, in which case we want to make sure we don't interfere with that */ if (!PA_SINK_INPUT_IS_LINKED(pa_sink_input_get_state(si))) return; if (!(role = pa_proplist_gets(si->proplist, PA_PROP_MEDIA_ROLE))) role_index = get_role_index("none"); else role_index = get_role_index(role); if (PA_INVALID_INDEX == role_index) return; device_index = u->preferred_sinks[role_index]; if (PA_INVALID_INDEX == device_index) return; if (!(sink = pa_idxset_get_by_index(u->core->sinks, device_index))) return; if (si->sink != sink) pa_sink_input_move_to(si, sink, FALSE); } static pa_hook_result_t route_sink_inputs(struct userdata *u, pa_sink *ignore_sink) { pa_sink_input *si; uint32_t idx; pa_assert(u); if (!u->do_routing) return PA_HOOK_OK; update_highest_priority_device_indexes(u, "sink:", ignore_sink); PA_IDXSET_FOREACH(si, u->core->sink_inputs, idx) { route_sink_input(u, si); } return PA_HOOK_OK; } static void route_source_output(struct userdata *u, pa_source_output *so) { const char *role; uint32_t role_index, device_index; pa_source *source; pa_assert(u); pa_assert(u->do_routing); if (so->save_source) return; if (so->direct_on_input) return; /* Skip this if it is already in the process of being moved anyway */ if (!so->source) return; /* It might happen that a stream and a source are set up at the same time, in which case we want to make sure we don't interfere with that */ if (!PA_SOURCE_OUTPUT_IS_LINKED(pa_source_output_get_state(so))) return; if (!(role = pa_proplist_gets(so->proplist, PA_PROP_MEDIA_ROLE))) role_index = get_role_index("none"); else role_index = get_role_index(role); if (PA_INVALID_INDEX == role_index) return; device_index = u->preferred_sources[role_index]; if (PA_INVALID_INDEX == device_index) return; if (!(source = pa_idxset_get_by_index(u->core->sources, device_index))) return; if (so->source != source) pa_source_output_move_to(so, source, FALSE); } static pa_hook_result_t route_source_outputs(struct userdata *u, pa_source* ignore_source) { pa_source_output *so; uint32_t idx; pa_assert(u); if (!u->do_routing) return PA_HOOK_OK; update_highest_priority_device_indexes(u, "source:", ignore_source); PA_IDXSET_FOREACH(so, u->core->source_outputs, idx) { route_source_output(u, so); } return PA_HOOK_OK; } static void subscribe_callback(pa_core *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) { struct userdata *u = userdata; struct entry entry, *old = NULL; char *name = NULL; pa_datum key, data; pa_assert(c); pa_assert(u); if (t != (PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_NEW) && t != (PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_CHANGE) && t != (PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_NEW) && t != (PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_CHANGE) && /*t != (PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_NEW) &&*/ t != (PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_CHANGE) && /*t != (PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT|PA_SUBSCRIPTION_EVENT_NEW) &&*/ t != (PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT|PA_SUBSCRIPTION_EVENT_CHANGE)) return; pa_zero(entry); entry.version = ENTRY_VERSION; if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { pa_sink_input *si; if (!u->do_routing) return; if (!(si = pa_idxset_get_by_index(c->sink_inputs, idx))) return; /* The role may change mid-stream, so we reroute */ route_sink_input(u, si); return; } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { pa_source_output *so; if (!u->do_routing) return; if (!(so = pa_idxset_get_by_index(c->source_outputs, idx))) return; /* The role may change mid-stream, so we reroute */ route_source_output(u, so); return; } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK) { pa_sink *sink; if (!(sink = pa_idxset_get_by_index(c->sinks, idx))) return; name = pa_sprintf_malloc("sink:%s", sink->name); old = load_or_initialize_entry(u, &entry, name, "sink:"); if (!entry.user_set_description) pa_strlcpy(entry.description, pa_strnull(pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_DESCRIPTION)), sizeof(entry.description)); else if (strncmp(entry.description, pa_strnull(pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_DESCRIPTION)), sizeof(entry.description)) != 0) { /* Warning: If two modules fight over the description, this could cause an infinite loop. by changing the description here, we retrigger this subscription callback. The only thing stopping us from looping is the fact that the string comparison will fail on the second iteration. If another module tries to manage the description, this will fail... */ pa_sink_set_description(sink, entry.description); } pa_strlcpy(entry.icon, pa_strnull(pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_ICON_NAME)), sizeof(entry.icon)); } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE) { pa_source *source; pa_assert((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE); if (!(source = pa_idxset_get_by_index(c->sources, idx))) return; if (source->monitor_of) return; name = pa_sprintf_malloc("source:%s", source->name); old = load_or_initialize_entry(u, &entry, name, "source:"); if (!entry.user_set_description) pa_strlcpy(entry.description, pa_strnull(pa_proplist_gets(source->proplist, PA_PROP_DEVICE_DESCRIPTION)), sizeof(entry.description)); else if (strncmp(entry.description, pa_strnull(pa_proplist_gets(source->proplist, PA_PROP_DEVICE_DESCRIPTION)), sizeof(entry.description)) != 0) { /* Warning: If two modules fight over the description, this could cause an infinite loop. by changing the description here, we retrigger this subscription callback. The only thing stopping us from looping is the fact that the string comparison will fail on the second iteration. If another module tries to manage the description, this will fail... */ pa_source_set_description(source, entry.description); } pa_strlcpy(entry.icon, pa_strnull(pa_proplist_gets(source->proplist, PA_PROP_DEVICE_ICON_NAME)), sizeof(entry.icon)); } pa_assert(name); if (old) { if (entries_equal(old, &entry)) { pa_xfree(old); pa_xfree(name); return; } pa_xfree(old); } key.data = name; key.size = strlen(name); data.data = &entry; data.size = sizeof(entry); pa_log_info("Storing device %s.", name); if (pa_database_set(u->database, &key, &data, TRUE) == 0) trigger_save(u); else pa_log_warn("Could not save device");; pa_xfree(name); } static pa_hook_result_t sink_new_hook_callback(pa_core *c, pa_sink_new_data *new_data, struct userdata *u) { char *name; struct entry *e; pa_assert(c); pa_assert(new_data); pa_assert(u); name = pa_sprintf_malloc("sink:%s", new_data->name); if ((e = read_entry(u, name))) { if (e->user_set_description && strncmp(e->description, pa_proplist_gets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION), sizeof(e->description)) != 0) { pa_log_info("Restoring description for sink %s.", new_data->name); pa_proplist_sets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION, e->description); } pa_xfree(e); } pa_xfree(name); return PA_HOOK_OK; } static pa_hook_result_t source_new_hook_callback(pa_core *c, pa_source_new_data *new_data, struct userdata *u) { char *name; struct entry *e; pa_assert(c); pa_assert(new_data); pa_assert(u); name = pa_sprintf_malloc("source:%s", new_data->name); if ((e = read_entry(u, name))) { if (e->user_set_description && strncmp(e->description, pa_proplist_gets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION), sizeof(e->description)) != 0) { /* NB, We cannot detect if we are a monitor here... this could mess things up a bit... */ pa_log_info("Restoring description for source %s.", new_data->name); pa_proplist_sets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION, e->description); } pa_xfree(e); } pa_xfree(name); return PA_HOOK_OK; } static pa_hook_result_t sink_input_new_hook_callback(pa_core *c, pa_sink_input_new_data *new_data, struct userdata *u) { const char *role; uint32_t role_index; pa_assert(c); pa_assert(new_data); pa_assert(u); if (!u->do_routing) return PA_HOOK_OK; if (new_data->sink) pa_log_debug("Not restoring device for stream because already set."); else { if (!(role = pa_proplist_gets(new_data->proplist, PA_PROP_MEDIA_ROLE))) role_index = get_role_index("none"); else role_index = get_role_index(role); if (PA_INVALID_INDEX != role_index) { uint32_t device_index; device_index = u->preferred_sinks[role_index]; if (PA_INVALID_INDEX != device_index) { pa_sink *sink; if ((sink = pa_idxset_get_by_index(u->core->sinks, device_index))) { new_data->sink = sink; new_data->save_sink = FALSE; } } } } return PA_HOOK_OK; } static pa_hook_result_t source_output_new_hook_callback(pa_core *c, pa_source_output_new_data *new_data, struct userdata *u) { const char *role; uint32_t role_index; pa_assert(c); pa_assert(new_data); pa_assert(u); if (!u->do_routing) return PA_HOOK_OK; if (new_data->direct_on_input) return PA_HOOK_OK; if (new_data->source) pa_log_debug("Not restoring device for stream because already set."); else { if (!(role = pa_proplist_gets(new_data->proplist, PA_PROP_MEDIA_ROLE))) role_index = get_role_index("none"); else role_index = get_role_index(role); if (PA_INVALID_INDEX != role_index) { uint32_t device_index; device_index = u->preferred_sources[role_index]; if (PA_INVALID_INDEX != device_index) { pa_source *source; if ((source = pa_idxset_get_by_index(u->core->sources, device_index))) { new_data->source = source; new_data->save_source = FALSE; } } } } return PA_HOOK_OK; } static pa_hook_result_t sink_put_hook_callback(pa_core *c, PA_GCC_UNUSED pa_sink *sink, struct userdata *u) { pa_assert(c); pa_assert(u); pa_assert(u->core == c); pa_assert(u->on_hotplug); notify_subscribers(u); return route_sink_inputs(u, NULL); } static pa_hook_result_t source_put_hook_callback(pa_core *c, PA_GCC_UNUSED pa_source *source, struct userdata *u) { pa_assert(c); pa_assert(u); pa_assert(u->core == c); pa_assert(u->on_hotplug); notify_subscribers(u); return route_source_outputs(u, NULL); } static pa_hook_result_t sink_unlink_hook_callback(pa_core *c, pa_sink *sink, struct userdata *u) { pa_assert(c); pa_assert(sink); pa_assert(u); pa_assert(u->core == c); pa_assert(u->on_rescue); /* There's no point in doing anything if the core is shut down anyway */ if (c->state == PA_CORE_SHUTDOWN) return PA_HOOK_OK; notify_subscribers(u); return route_sink_inputs(u, sink); } static pa_hook_result_t source_unlink_hook_callback(pa_core *c, pa_source *source, struct userdata *u) { pa_assert(c); pa_assert(source); pa_assert(u); pa_assert(u->core == c); pa_assert(u->on_rescue); /* There's no point in doing anything if the core is shut down anyway */ if (c->state == PA_CORE_SHUTDOWN) return PA_HOOK_OK; notify_subscribers(u); return route_source_outputs(u, source); } static void apply_entry(struct userdata *u, const char *name, struct entry *e) { uint32_t idx; char *n; pa_assert(u); pa_assert(name); pa_assert(e); if (!e->user_set_description) return; if ((n = get_name(name, "sink:"))) { pa_sink *s; PA_IDXSET_FOREACH(s, u->core->sinks, idx) { if (!pa_streq(s->name, n)) { continue; } pa_log_info("Setting description for sink %s to '%s'", s->name, e->description); pa_sink_set_description(s, e->description); } pa_xfree(n); } else if ((n = get_name(name, "source:"))) { pa_source *s; PA_IDXSET_FOREACH(s, u->core->sources, idx) { if (!pa_streq(s->name, n)) { continue; } if (s->monitor_of) { pa_log_warn("Cowardly refusing to set the description for monitor source %s.", s->name); continue; } pa_log_info("Setting description for source %s to '%s'", s->name, e->description); pa_source_set_description(s, e->description); } pa_xfree(n); } } #define EXT_VERSION 1 static int extension_cb(pa_native_protocol *p, pa_module *m, pa_native_connection *c, uint32_t tag, pa_tagstruct *t) { struct userdata *u; uint32_t command; pa_tagstruct *reply = NULL; pa_assert(p); pa_assert(m); pa_assert(c); pa_assert(t); u = m->userdata; if (pa_tagstruct_getu32(t, &command) < 0) goto fail; reply = pa_tagstruct_new(NULL, 0); pa_tagstruct_putu32(reply, PA_COMMAND_REPLY); pa_tagstruct_putu32(reply, tag); switch (command) { case SUBCOMMAND_TEST: { if (!pa_tagstruct_eof(t)) goto fail; pa_tagstruct_putu32(reply, EXT_VERSION); break; } case SUBCOMMAND_READ: { pa_datum key; pa_bool_t done; if (!pa_tagstruct_eof(t)) goto fail; done = !pa_database_first(u->database, &key, NULL); while (!done) { pa_datum next_key; struct entry *e; char *name; done = !pa_database_next(u->database, &key, &next_key, NULL); name = pa_xstrndup(key.data, key.size); pa_datum_free(&key); if ((e = read_entry(u, name))) { uint32_t idx; char *device_name; uint32_t found_index = PA_INVALID_INDEX; if ((device_name = get_name(name, "sink:"))) { pa_sink* s; PA_IDXSET_FOREACH(s, u->core->sinks, idx) { if (strcmp(s->name, device_name) == 0) { found_index = s->index; break; } } pa_xfree(device_name); } else if ((device_name = get_name(name, "source:"))) { pa_source* s; PA_IDXSET_FOREACH(s, u->core->sources, idx) { if (strcmp(s->name, device_name) == 0) { found_index = s->index; break; } } pa_xfree(device_name); } pa_tagstruct_puts(reply, name); pa_tagstruct_puts(reply, e->description); pa_tagstruct_puts(reply, e->icon); pa_tagstruct_putu32(reply, found_index); pa_tagstruct_putu32(reply, NUM_ROLES); for (uint32_t i = ROLE_NONE; i < NUM_ROLES; ++i) { pa_tagstruct_puts(reply, role_names[i]); pa_tagstruct_putu32(reply, e->priority[i]); } pa_xfree(e); } pa_xfree(name); key = next_key; } break; } case SUBCOMMAND_RENAME: { struct entry *e; const char *device, *description; if (pa_tagstruct_gets(t, &device) < 0 || pa_tagstruct_gets(t, &description) < 0) goto fail; if (!device || !*device || !description || !*description) goto fail; if ((e = read_entry(u, device))) { pa_datum key, data; pa_strlcpy(e->description, description, sizeof(e->description)); e->user_set_description = TRUE; key.data = (char *) device; key.size = strlen(device); data.data = e; data.size = sizeof(*e); if (pa_database_set(u->database, &key, &data, TRUE) == 0) { apply_entry(u, device, e); trigger_save(u); } else pa_log_warn("Could not save device"); pa_xfree(e); } else pa_log_warn("Could not rename device %s, no entry in database", device); break; } case SUBCOMMAND_DELETE: while (!pa_tagstruct_eof(t)) { const char *name; pa_datum key; if (pa_tagstruct_gets(t, &name) < 0) goto fail; key.data = (char*) name; key.size = strlen(name); /** @todo: Reindex the priorities */ pa_database_unset(u->database, &key); } trigger_save(u); break; case SUBCOMMAND_ROLE_DEVICE_PRIORITY_ROUTING: { pa_bool_t enable; if (pa_tagstruct_get_boolean(t, &enable) < 0) goto fail; if ((u->do_routing = enable)) { /* Update our caches */ update_highest_priority_device_indexes(u, "sink:", NULL); update_highest_priority_device_indexes(u, "source:", NULL); } break; } case SUBCOMMAND_REORDER: { const char *role; struct entry *e; uint32_t role_index, n_devices; pa_datum key, data; pa_bool_t done, sink_mode = TRUE; struct device_t { uint32_t prio; char *device; }; struct device_t *device; struct device_t **devices; uint32_t i, idx, offset; pa_hashmap *h; /*void *state;*/ pa_bool_t first; if (pa_tagstruct_gets(t, &role) < 0 || pa_tagstruct_getu32(t, &n_devices) < 0 || n_devices < 1) goto fail; if (PA_INVALID_INDEX == (role_index = get_role_index(role))) goto fail; /* Cycle through the devices given and make sure they exist */ h = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); first = TRUE; idx = 0; for (i = 0; i < n_devices; ++i) { const char *s; if (pa_tagstruct_gets(t, &s) < 0) { while ((device = pa_hashmap_steal_first(h))) { pa_xfree(device->device); pa_xfree(device); } pa_hashmap_free(h, NULL, NULL); pa_log_error("Protocol error on reorder"); goto fail; } /* Ensure this is a valid entry */ if (!(e = read_entry(u, s))) { while ((device = pa_hashmap_steal_first(h))) { pa_xfree(device->device); pa_xfree(device); } pa_hashmap_free(h, NULL, NULL); pa_log_error("Client specified an unknown device in it's reorder list."); goto fail; } pa_xfree(e); if (first) { first = FALSE; sink_mode = (0 == strncmp("sink:", s, 5)); } else if ((sink_mode && 0 != strncmp("sink:", s, 5)) || (!sink_mode && 0 != strncmp("source:", s, 7))) { while ((device = pa_hashmap_steal_first(h))) { pa_xfree(device->device); pa_xfree(device); } pa_hashmap_free(h, NULL, NULL); pa_log_error("Attempted to reorder mixed devices (sinks and sources)"); goto fail; } /* Add the device to our hashmap. If it's alredy in it, free it now and carry on */ device = pa_xnew(struct device_t, 1); device->device = pa_xstrdup(s); if (pa_hashmap_put(h, device->device, device) == 0) { device->prio = idx; idx++; } else { pa_xfree(device->device); pa_xfree(device); } } /*pa_log_debug("Hashmap contents (received from client)"); PA_HASHMAP_FOREACH(device, h, state) { pa_log_debug(" - %s (%d)", device->device, device->prio); }*/ /* Now cycle through our list and add all the devices. This has the effect of addign in any in our DB, not specified in the device list (and thus will be tacked on at the end) */ offset = idx; done = !pa_database_first(u->database, &key, NULL); while (!done && idx < 256) { pa_datum next_key; done = !pa_database_next(u->database, &key, &next_key, NULL); device = pa_xnew(struct device_t, 1); device->device = pa_xstrndup(key.data, key.size); if ((sink_mode && 0 == strncmp("sink:", device->device, 5)) || (!sink_mode && 0 == strncmp("source:", device->device, 7))) { /* Add the device to our hashmap. If it's alredy in it, free it now and carry on */ if (pa_hashmap_put(h, device->device, device) == 0 && (e = read_entry(u, device->device))) { /* We add offset on to the existing priorirty so that when we order, the existing entries are always lower priority than the new ones. */ device->prio = (offset + e->priority[role_index]); pa_xfree(e); } else { pa_xfree(device->device); pa_xfree(device); } } else { pa_xfree(device->device); pa_xfree(device); } pa_datum_free(&key); key = next_key; } /*pa_log_debug("Hashmap contents (combined with database)"); PA_HASHMAP_FOREACH(device, h, state) { pa_log_debug(" - %s (%d)", device->device, device->prio); }*/ /* Now we put all the entries in a simple list for sorting it. */ n_devices = pa_hashmap_size(h); devices = pa_xnew(struct device_t *, n_devices); idx = 0; while ((device = pa_hashmap_steal_first(h))) { devices[idx++] = device; } pa_hashmap_free(h, NULL, NULL); /* Simple bubble sort */ for (i = 0; i < n_devices; ++i) { for (uint32_t j = i; j < n_devices; ++j) { if (devices[i]->prio > devices[j]->prio) { struct device_t *tmp; tmp = devices[i]; devices[i] = devices[j]; devices[j] = tmp; } } } /*pa_log_debug("Sorted device list"); for (i = 0; i < n_devices; ++i) { pa_log_debug(" - %s (%d)", devices[i]->device, devices[i]->prio); }*/ /* Go through in order and write the new entry and cleanup our own list */ idx = 1; first = TRUE; for (i = 0; i < n_devices; ++i) { if ((e = read_entry(u, devices[i]->device))) { if (e->priority[role_index] == idx) idx++; else { e->priority[role_index] = idx; key.data = (char *) devices[i]->device; key.size = strlen(devices[i]->device); data.data = e; data.size = sizeof(*e); if (pa_database_set(u->database, &key, &data, TRUE) == 0) { first = FALSE; idx++; } } pa_xfree(e); } pa_xfree(devices[i]->device); pa_xfree(devices[i]); } if (!first) { trigger_save(u); if (sink_mode) route_sink_inputs(u, NULL); else route_source_outputs(u, NULL); } break; } case SUBCOMMAND_SUBSCRIBE: { pa_bool_t enabled; if (pa_tagstruct_get_boolean(t, &enabled) < 0 || !pa_tagstruct_eof(t)) goto fail; if (enabled) pa_idxset_put(u->subscribed, c, NULL); else pa_idxset_remove_by_data(u->subscribed, c, NULL); break; } default: goto fail; } pa_pstream_send_tagstruct(pa_native_connection_get_pstream(c), reply); return 0; fail: if (reply) pa_tagstruct_free(reply); return -1; } static pa_hook_result_t connection_unlink_hook_cb(pa_native_protocol *p, pa_native_connection *c, struct userdata *u) { pa_assert(p); pa_assert(c); pa_assert(u); pa_idxset_remove_by_data(u->subscribed, c, NULL); return PA_HOOK_OK; } struct prioritised_indexes { uint32_t index; int32_t priority; }; int pa__init(pa_module*m) { pa_modargs *ma = NULL; struct userdata *u; char *fname; pa_sink *sink; pa_source *source; uint32_t idx; pa_bool_t do_routing = FALSE, on_hotplug = TRUE, on_rescue = TRUE; uint32_t total_devices; pa_assert(m); if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { pa_log("Failed to parse module arguments"); goto fail; } if (pa_modargs_get_value_boolean(ma, "do_routing", &do_routing) < 0 || pa_modargs_get_value_boolean(ma, "on_hotplug", &on_hotplug) < 0 || pa_modargs_get_value_boolean(ma, "on_rescue", &on_rescue) < 0) { pa_log("on_hotplug= and on_rescue= expect boolean arguments"); goto fail; } m->userdata = u = pa_xnew0(struct userdata, 1); u->core = m->core; u->module = m; u->do_routing = do_routing; u->on_hotplug = on_hotplug; u->on_rescue = on_rescue; u->subscribed = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); u->protocol = pa_native_protocol_get(m->core); pa_native_protocol_install_ext(u->protocol, m, extension_cb); u->connection_unlink_hook_slot = pa_hook_connect(&pa_native_protocol_hooks(u->protocol)[PA_NATIVE_HOOK_CONNECTION_UNLINK], PA_HOOK_NORMAL, (pa_hook_cb_t) connection_unlink_hook_cb, u); u->subscription = pa_subscription_new(m->core, PA_SUBSCRIPTION_MASK_SINK|PA_SUBSCRIPTION_MASK_SOURCE|PA_SUBSCRIPTION_MASK_SINK_INPUT|PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT, subscribe_callback, u); /* Used to handle device description management */ u->sink_new_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_NEW], PA_HOOK_EARLY, (pa_hook_cb_t) sink_new_hook_callback, u); u->source_new_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_NEW], PA_HOOK_EARLY, (pa_hook_cb_t) source_new_hook_callback, u); /* The following slots are used to deal with routing */ /* A little bit later than module-stream-restore, but before module-intended-roles */ u->sink_input_new_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_NEW], PA_HOOK_EARLY+5, (pa_hook_cb_t) sink_input_new_hook_callback, u); u->source_output_new_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_NEW], PA_HOOK_EARLY+5, (pa_hook_cb_t) source_output_new_hook_callback, u); if (on_hotplug) { /* A little bit later than module-stream-restore, but before module-intended-roles */ u->sink_put_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT], PA_HOOK_LATE+5, (pa_hook_cb_t) sink_put_hook_callback, u); u->source_put_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT], PA_HOOK_LATE+5, (pa_hook_cb_t) source_put_hook_callback, u); } if (on_rescue) { /* A little bit later than module-stream-restore, a little bit earlier than module-intended-roles, module-rescue-streams, ... */ u->sink_unlink_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_UNLINK], PA_HOOK_LATE+5, (pa_hook_cb_t) sink_unlink_hook_callback, u); u->source_unlink_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_UNLINK], PA_HOOK_LATE+5, (pa_hook_cb_t) source_unlink_hook_callback, u); } if (!(fname = pa_state_path("device-manager", TRUE))) goto fail; if (!(u->database = pa_database_open(fname, TRUE))) { pa_log("Failed to open volume database '%s': %s", fname, pa_cstrerror(errno)); pa_xfree(fname); goto fail; } pa_log_info("Successfully opened database file '%s'.", fname); pa_xfree(fname); /* Attempt to inject the devices into the list in priority order */ total_devices = PA_MAX(pa_idxset_size(m->core->sinks), pa_idxset_size(m->core->sources)); if (total_devices > 0 && total_devices < 128) { uint32_t i; struct prioritised_indexes p_i[128]; /* We cycle over all the available sinks so that they are added to our database if they are not in it yet */ i = 0; PA_IDXSET_FOREACH(sink, m->core->sinks, idx) { pa_log_debug("Found sink index %u", sink->index); p_i[i ].index = sink->index; p_i[i++].priority = sink->priority; } /* Bubble sort it (only really useful for first time creation) */ if (i > 1) for (uint32_t j = 0; j < i; ++j) for (uint32_t k = 0; k < i; ++k) if (p_i[j].priority > p_i[k].priority) { struct prioritised_indexes tmp_pi = p_i[k]; p_i[k] = p_i[j]; p_i[j] = tmp_pi; } /* Register it */ for (uint32_t j = 0; j < i; ++j) subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_NEW, p_i[j].index, u); /* We cycle over all the available sources so that they are added to our database if they are not in it yet */ i = 0; PA_IDXSET_FOREACH(source, m->core->sources, idx) { p_i[i ].index = source->index; p_i[i++].priority = source->priority; } /* Bubble sort it (only really useful for first time creation) */ if (i > 1) for (uint32_t j = 0; j < i; ++j) for (uint32_t k = 0; k < i; ++k) if (p_i[j].priority > p_i[k].priority) { struct prioritised_indexes tmp_pi = p_i[k]; p_i[k] = p_i[j]; p_i[j] = tmp_pi; } /* Register it */ for (uint32_t j = 0; j < i; ++j) subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_NEW, p_i[j].index, u); } else if (total_devices > 0) { /* This user has a *lot* of devices... */ PA_IDXSET_FOREACH(sink, m->core->sinks, idx) subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_NEW, sink->index, u); PA_IDXSET_FOREACH(source, m->core->sources, idx) subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_NEW, source->index, u); } /* Perform the routing (if it's enabled) which will update our priority list cache too */ for (uint32_t i = 0; i < NUM_ROLES; ++i) { u->preferred_sinks[i] = u->preferred_sources[i] = PA_INVALID_INDEX; } route_sink_inputs(u, NULL); route_source_outputs(u, NULL); #ifdef DUMP_DATABASE dump_database(u); #endif pa_modargs_free(ma); return 0; fail: pa__done(m); if (ma) pa_modargs_free(ma); return -1; } void pa__done(pa_module*m) { struct userdata* u; pa_assert(m); if (!(u = m->userdata)) return; if (u->subscription) pa_subscription_free(u->subscription); if (u->sink_new_hook_slot) pa_hook_slot_free(u->sink_new_hook_slot); if (u->source_new_hook_slot) pa_hook_slot_free(u->source_new_hook_slot); if (u->sink_input_new_hook_slot) pa_hook_slot_free(u->sink_input_new_hook_slot); if (u->source_output_new_hook_slot) pa_hook_slot_free(u->source_output_new_hook_slot); if (u->sink_put_hook_slot) pa_hook_slot_free(u->sink_put_hook_slot); if (u->source_put_hook_slot) pa_hook_slot_free(u->source_put_hook_slot); if (u->sink_unlink_hook_slot) pa_hook_slot_free(u->sink_unlink_hook_slot); if (u->source_unlink_hook_slot) pa_hook_slot_free(u->source_unlink_hook_slot); if (u->save_time_event) u->core->mainloop->time_free(u->save_time_event); if (u->database) pa_database_close(u->database); if (u->protocol) { pa_native_protocol_remove_ext(u->protocol, m); pa_native_protocol_unref(u->protocol); } if (u->subscribed) pa_idxset_free(u->subscribed, NULL, NULL); pa_xfree(u); }