| /*** | |
| This file is part of PulseAudio. | |
| Copyright 2004-2006 Lennart Poettering | |
| 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, see <http://www.gnu.org/licenses/>. | |
| ***/ | |
| #ifdef HAVE_CONFIG_H | |
| #include <config.h> | |
| #endif | |
| #include <unistd.h> | |
| #include <string.h> | |
| #include <errno.h> | |
| #include <sys/types.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #if defined(HAVE_REGEX_H) | |
| #include <regex.h> | |
| #elif defined(HAVE_PCREPOSIX_H) | |
| #include <pcreposix.h> | |
| #endif | |
| #include <pulse/xmalloc.h> | |
| #include <pulsecore/core-error.h> | |
| #include <pulsecore/module.h> | |
| #include <pulsecore/core-util.h> | |
| #include <pulsecore/modargs.h> | |
| #include <pulsecore/log.h> | |
| #include <pulsecore/sink-input.h> | |
| #include <pulsecore/core-util.h> | |
| PA_MODULE_AUTHOR("Lennart Poettering"); | |
| PA_MODULE_DESCRIPTION("Playback stream expression matching module"); | |
| PA_MODULE_VERSION(PACKAGE_VERSION); | |
| PA_MODULE_LOAD_ONCE(false); | |
| PA_MODULE_USAGE("table=<filename> " | |
| "key=<property_key>"); | |
| #define WHITESPACE "\n\r \t" | |
| #define DEFAULT_MATCH_TABLE_FILE PA_DEFAULT_CONFIG_DIR"/match.table" | |
| #define DEFAULT_MATCH_TABLE_FILE_USER "match.table" | |
| #define UPDATE_REPLACE "replace" | |
| #define UPDATE_MERGE "merge" | |
| static const char* const valid_modargs[] = { | |
| "table", | |
| "key", | |
| NULL, | |
| }; | |
| struct rule { | |
| regex_t regex; | |
| pa_volume_t volume; | |
| pa_proplist *proplist; | |
| pa_update_mode_t mode; | |
| struct rule *next; | |
| }; | |
| struct userdata { | |
| struct rule *rules; | |
| char *property_key; | |
| pa_hook_slot *sink_input_fixate_hook_slot; | |
| }; | |
| static int load_rules(struct userdata *u, const char *filename) { | |
| FILE *f; | |
| int n = 0; | |
| int ret = -1; | |
| struct rule *end = NULL; | |
| char *fn = NULL; | |
| pa_assert(u); | |
| if (filename) | |
| f = pa_fopen_cloexec(fn = pa_xstrdup(filename), "r"); | |
| else | |
| f = pa_open_config_file(DEFAULT_MATCH_TABLE_FILE, DEFAULT_MATCH_TABLE_FILE_USER, NULL, &fn); | |
| if (!f) { | |
| pa_log("Failed to open file config file: %s", pa_cstrerror(errno)); | |
| goto finish; | |
| } | |
| pa_lock_fd(fileno(f), 1); | |
| while (!feof(f)) { | |
| char *token_end, *value_str; | |
| pa_volume_t volume = PA_VOLUME_NORM; | |
| uint32_t k; | |
| regex_t regex; | |
| char ln[256]; | |
| struct rule *rule; | |
| pa_proplist *proplist = NULL; | |
| pa_update_mode_t mode = (pa_update_mode_t) -1; | |
| if (!fgets(ln, sizeof(ln), f)) | |
| break; | |
| n++; | |
| pa_strip_nl(ln); | |
| if (ln[0] == '#' || !*ln ) | |
| continue; | |
| token_end = ln + strcspn(ln, WHITESPACE); | |
| value_str = token_end + strspn(token_end, WHITESPACE); | |
| *token_end = 0; | |
| if (!*ln) { | |
| pa_log("[%s:%u] failed to parse line - missing regexp", fn, n); | |
| goto finish; | |
| } | |
| if (!*value_str) { | |
| pa_log("[%s:%u] failed to parse line - too few words", fn, n); | |
| goto finish; | |
| } | |
| if (pa_atou(value_str, &k) >= 0) | |
| volume = (pa_volume_t) PA_CLAMP_VOLUME(k); | |
| else { | |
| size_t len; | |
| token_end = value_str + strcspn(value_str, WHITESPACE); | |
| len = token_end - value_str; | |
| if (len == (sizeof(UPDATE_REPLACE) - 1) && !strncmp(value_str, UPDATE_REPLACE, len)) | |
| mode = PA_UPDATE_REPLACE; | |
| else if (len == (sizeof(UPDATE_MERGE) - 1) && !strncmp(value_str, UPDATE_MERGE, len)) | |
| mode = PA_UPDATE_MERGE; | |
| if (mode != (pa_update_mode_t) -1) { | |
| value_str = token_end + strspn(token_end, WHITESPACE); | |
| if (!*value_str) { | |
| pa_log("[%s:%u] failed to parse line - too few words", fn, n); | |
| goto finish; | |
| } | |
| } else | |
| mode = PA_UPDATE_MERGE; | |
| if (*value_str == '"') { | |
| value_str++; | |
| token_end = strchr(value_str, '"'); | |
| if (!token_end) { | |
| pa_log("[%s:%u] failed to parse line - missing role closing quote", fn, n); | |
| goto finish; | |
| } | |
| } else | |
| token_end = value_str + strcspn(value_str, WHITESPACE); | |
| *token_end = 0; | |
| value_str = pa_sprintf_malloc("media.role=\"%s\"", value_str); | |
| proplist = pa_proplist_from_string(value_str); | |
| pa_xfree(value_str); | |
| } | |
| if (regcomp(®ex, ln, REG_EXTENDED|REG_NOSUB) != 0) { | |
| pa_log("[%s:%u] invalid regular expression", fn, n); | |
| if (proplist) | |
| pa_proplist_free(proplist); | |
| goto finish; | |
| } | |
| rule = pa_xnew(struct rule, 1); | |
| rule->regex = regex; | |
| rule->proplist = proplist; | |
| rule->mode = mode; | |
| rule->volume = volume; | |
| rule->next = NULL; | |
| if (end) | |
| end->next = rule; | |
| else | |
| u->rules = rule; | |
| end = rule; | |
| } | |
| ret = 0; | |
| finish: | |
| if (f) { | |
| pa_lock_fd(fileno(f), 0); | |
| fclose(f); | |
| } | |
| pa_xfree(fn); | |
| return ret; | |
| } | |
| static pa_hook_result_t sink_input_fixate_hook_callback(pa_core *c, pa_sink_input_new_data *si, struct userdata *u) { | |
| struct rule *r; | |
| const char *n; | |
| pa_assert(c); | |
| pa_assert(u); | |
| if (!(n = pa_proplist_gets(si->proplist, u->property_key))) | |
| return PA_HOOK_OK; | |
| pa_log_debug("Matching with %s", n); | |
| for (r = u->rules; r; r = r->next) { | |
| if (!regexec(&r->regex, n, 0, NULL, 0)) { | |
| if (r->proplist) { | |
| pa_log_debug("updating proplist of sink input '%s'", n); | |
| pa_proplist_update(si->proplist, r->mode, r->proplist); | |
| } else if (si->volume_writable) { | |
| pa_cvolume cv; | |
| pa_log_debug("changing volume of sink input '%s' to 0x%03x", n, r->volume); | |
| pa_cvolume_set(&cv, si->sample_spec.channels, r->volume); | |
| pa_sink_input_new_data_set_volume(si, &cv); | |
| } else | |
| pa_log_debug("the volume of sink input '%s' is not writable, can't change it", n); | |
| } | |
| } | |
| return PA_HOOK_OK; | |
| } | |
| int pa__init(pa_module*m) { | |
| pa_modargs *ma = NULL; | |
| struct userdata *u; | |
| pa_assert(m); | |
| if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { | |
| pa_log("Failed to parse module arguments"); | |
| goto fail; | |
| } | |
| u = pa_xnew0(struct userdata, 1); | |
| u->rules = NULL; | |
| m->userdata = u; | |
| u->property_key = pa_xstrdup(pa_modargs_get_value(ma, "key", PA_PROP_MEDIA_NAME)); | |
| if (load_rules(u, pa_modargs_get_value(ma, "table", NULL)) < 0) | |
| goto fail; | |
| /* hook EARLY - 1, to match before stream-restore */ | |
| u->sink_input_fixate_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_FIXATE], PA_HOOK_EARLY - 1, (pa_hook_cb_t) sink_input_fixate_hook_callback, u); | |
| 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; | |
| struct rule *r, *n; | |
| pa_assert(m); | |
| if (!(u = m->userdata)) | |
| return; | |
| if (u->sink_input_fixate_hook_slot) | |
| pa_hook_slot_free(u->sink_input_fixate_hook_slot); | |
| if (u->property_key) | |
| pa_xfree(u->property_key); | |
| for (r = u->rules; r; r = n) { | |
| n = r->next; | |
| regfree(&r->regex); | |
| if (r->proplist) | |
| pa_proplist_free(r->proplist); | |
| pa_xfree(r); | |
| } | |
| pa_xfree(u); | |
| } |