From 777bc89d020b8e9306d39770ee420d6ccf6ba7dd Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Fri, 23 Jul 2021 18:50:33 +0300 Subject: [PATCH] pipewire-pulse: add module-switch-on-connect module-switch-on-connect sets the configured default sink/source whenever suitable new sink/sources appear. This should give the same behavior as Pulseaudio's module. This module exists mainly to provide a workaround e.g. for desktop environments such as XFCE, whose mixer applications try to manage the default devices assuming fully PA-like behavior, breaking default pipewire output switching. --- src/modules/meson.build | 1 + src/modules/module-protocol-pulse/module.c | 1 + .../modules/module-switch-on-connect.c | 327 ++++++++++++++++++ .../module-protocol-pulse/modules/registry.h | 1 + 4 files changed, 330 insertions(+) create mode 100644 src/modules/module-protocol-pulse/modules/module-switch-on-connect.c diff --git a/src/modules/meson.build b/src/modules/meson.build index af8dfb647..7aec88e87 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -194,6 +194,7 @@ pipewire_module_protocol_pulse_sources = [ 'module-protocol-pulse/modules/module-remap-sink.c', 'module-protocol-pulse/modules/module-remap-source.c', 'module-protocol-pulse/modules/module-simple-protocol-tcp.c', + 'module-protocol-pulse/modules/module-switch-on-connect.c', 'module-protocol-pulse/modules/module-tunnel-sink.c', 'module-protocol-pulse/modules/module-tunnel-source.c', 'module-protocol-pulse/modules/module-zeroconf-discover.c', diff --git a/src/modules/module-protocol-pulse/module.c b/src/modules/module-protocol-pulse/module.c index ed64b8880..af267c1ea 100644 --- a/src/modules/module-protocol-pulse/module.c +++ b/src/modules/module-protocol-pulse/module.c @@ -242,6 +242,7 @@ static const struct module_info module_list[] = { { "module-remap-sink", create_module_remap_sink, }, { "module-remap-source", create_module_remap_source, }, { "module-simple-protocol-tcp", create_module_simple_protocol_tcp, }, + { "module-switch-on-connect", create_module_switch_on_connect, }, { "module-tunnel-sink", create_module_tunnel_sink, }, { "module-tunnel-source", create_module_tunnel_source, }, { "module-zeroconf-discover", create_module_zeroconf_discover, }, diff --git a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c new file mode 100644 index 000000000..5a0eb3bc0 --- /dev/null +++ b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c @@ -0,0 +1,327 @@ +/* PipeWire + * + * Copyright © 2021 Wim Taymans + * Copyright © 2021 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include + +#include "../defs.h" +#include "../module.h" +#include "registry.h" + +#include "../manager.h" +#include "../collect.h" + +#define NAME "pulse-server: module-switch-on-connect" + +/* Ignore HDMI by default */ +#define DEFAULT_BLOCKLIST "hdmi" + +struct module_switch_on_connect_data { + struct module *module; + + struct pw_core *core; + struct pw_manager *manager; + struct spa_hook core_listener; + struct spa_hook manager_listener; + struct pw_manager_object *metadata_default; + + regex_t *blocklist; + + int sync_seq; + + unsigned int only_from_unavailable:1; + unsigned int ignore_virtual:1; + unsigned int started:1; +}; + +static void handle_metadata(struct module_switch_on_connect_data *d, struct pw_manager_object *old, + struct pw_manager_object *new, const char *name) +{ + if (spa_streq(name, "default")) { + if (d->metadata_default == old) + d->metadata_default = new; + } +} + +static void manager_added(void *data, struct pw_manager_object *o) +{ + struct module_switch_on_connect_data *d = data; + struct pw_node_info *info = o->info; + struct pw_device_info *card_info = NULL; + uint32_t card_id = SPA_ID_INVALID; + struct pw_manager_object *card = NULL; + const char *str, *bus, *name; + + if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) { + if (o->props != NULL && + (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL) + handle_metadata(d, NULL, o, str); + } + + if (!d->metadata_default || !d->started) + return; + + if (!(pw_manager_object_is_sink(o) || pw_manager_object_is_source_or_monitor(o))) + return; + + if (!info || !info->props) + return; + + name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME); + if (!name) + return; + + /* Find card */ + if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL) + card_id = (uint32_t)atoi(str); + if (card_id != SPA_ID_INVALID) { + struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, }; + card = select_object(d->manager, &sel); + } + if (!card) + return; + card_info = card->info; + if (!card_info || !card_info->props) + return; + + pw_log_debug(NAME ": considering switching to %s", name); + + /* If internal device, only consider hdmi sinks */ + str = spa_dict_lookup(info->props, "api.alsa.path"); + bus = spa_dict_lookup(card_info->props, PW_KEY_DEVICE_BUS); + if ((spa_streq(bus, "pci") || spa_streq(bus, "isa")) && + !(pw_manager_object_is_sink(o) && spa_strstartswith(str, "hdmi"))) { + pw_log_debug(NAME ": not switching to internal device"); + return; + } + + if (d->blocklist && regexec(d->blocklist, name, 0, NULL, 0) == 0) { + pw_log_debug(NAME ": not switching to blocklisted device"); + return; + } + + if (d->ignore_virtual && spa_dict_lookup(info->props, PW_KEY_DEVICE_API) == NULL) { + pw_log_debug(NAME ": not switching to virtual device"); + return; + } + + if (d->only_from_unavailable) { + /* XXX: not implemented */ + } + + /* Switch default */ + pw_log_debug(NAME ": switching to %s", name); + + pw_manager_set_metadata(d->manager, d->metadata_default, + PW_ID_CORE, + pw_manager_object_is_sink(o) ? METADATA_CONFIG_DEFAULT_SINK + : METADATA_CONFIG_DEFAULT_SOURCE, + "Spa:String:JSON", "{ \"name\": \"%s\" }", name); +} + +static void manager_sync(void *data) +{ + struct module_switch_on_connect_data *d = data; + + /* Manager emits devices/etc next --- enable started flag after that */ + if (!d->started) + d->sync_seq = pw_core_sync(d->core, PW_ID_CORE, d->sync_seq); +} + +static const struct pw_manager_events manager_events = { + PW_VERSION_MANAGER_EVENTS, + .added = manager_added, + .sync = manager_sync, +}; + +static void on_core_done(void *data, uint32_t id, int seq) +{ + struct module_switch_on_connect_data *d = data; + if (seq == d->sync_seq) { + pw_log_debug(NAME" %p: started", d); + d->started = true; + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .done = on_core_done, +}; + +static int module_switch_on_connect_load(struct client *client, struct module *module) +{ + struct impl *impl = client->impl; + struct module_switch_on_connect_data *d = module->user_data; + int res; + + d->core = pw_context_connect(impl->context, pw_properties_copy(client->props), 0); + if (d->core == NULL) { + res = -errno; + goto error; + } + + d->manager = pw_manager_new(d->core); + if (d->manager == NULL) { + res = -errno; + pw_core_disconnect(d->core); + d->core = NULL; + goto error; + } + + pw_manager_add_listener(d->manager, &d->manager_listener, &manager_events, d); + pw_core_add_listener(d->core, &d->core_listener, &core_events, d); + + /* Postpone setting started flag after initial nodes emitted */ + pw_manager_sync(d->manager); + + return 0; + +error: + pw_log_error(NAME" %p: failed to connect: %s", impl, spa_strerror(res)); + return res; +} + +static int module_switch_on_connect_unload(struct client *client, struct module *module) +{ + struct module_switch_on_connect_data *d = module->user_data; + + if (d->manager) { + spa_hook_remove(&d->manager_listener); + pw_manager_destroy(d->manager); + d->manager = NULL; + } + + if (d->core) { + spa_hook_remove(&d->core_listener); + pw_core_disconnect(d->core); + d->core = NULL; + } + + if (d->blocklist) { + regfree(d->blocklist); + free(d->blocklist); + d->blocklist = NULL; + } + + return 0; +} + +static const struct module_methods module_switch_on_connect_methods = { + VERSION_MODULE_METHODS, + .load = module_switch_on_connect_load, + .unload = module_switch_on_connect_unload, +}; + +static const struct spa_dict_item module_switch_on_connect_info[] = { + { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen " }, + { PW_KEY_MODULE_DESCRIPTION, "Switch to new devices on connect. " + "This module exists for Pulseaudio compatibility, and is useful only when some applications " + "try to manage the default sinks/sources themselves and interfere with PipeWire's builtin " + "default device switching." }, + { PW_KEY_MODULE_USAGE, "only_from_unavailable= " + "ignore_virtual= " + "blocklist= " }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct module *create_module_switch_on_connect(struct impl *impl, const char *argument) +{ + struct module *module; + struct module_switch_on_connect_data *d; + struct pw_properties *props = NULL; + regex_t *blocklist = NULL; + bool only_from_unavailable = false, ignore_virtual = true; + const char *str; + int res; + + props = pw_properties_new_dict(&SPA_DICT_INIT_ARRAY(module_switch_on_connect_info)); + if (!props) { + res = -EINVAL; + goto out; + } + if (argument) + module_args_add_props(props, argument); + + if ((str = pw_properties_get(props, "only_from_unavailable")) != NULL) { + only_from_unavailable = pw_properties_parse_bool(str); + pw_properties_set(props, "only_from_unavailable", NULL); + } + + if ((str = pw_properties_get(props, "ignore_virtual")) != NULL) { + ignore_virtual = pw_properties_parse_bool(str); + pw_properties_set(props, "ignore_virtual", NULL); + } + + if ((blocklist = malloc(sizeof(regex_t))) == NULL) { + res = -ENOMEM; + goto out; + } + + if ((str = pw_properties_get(props, "blocklist")) == NULL) + str = DEFAULT_BLOCKLIST; + + if ((res = regcomp(blocklist, str, REG_NOSUB | REG_EXTENDED)) != 0) { + free(blocklist); + blocklist = NULL; + res = -EINVAL; + goto out; + } + + if (str != DEFAULT_BLOCKLIST) + pw_properties_set(props, "blocklist", NULL); + + module = module_new(impl, &module_switch_on_connect_methods, sizeof(*d)); + if (module == NULL) { + res = -errno; + goto out; + } + + module->props = props; + d = module->user_data; + d->module = module; + d->blocklist = blocklist; + d->ignore_virtual = ignore_virtual; + d->only_from_unavailable = only_from_unavailable; + + if (d->only_from_unavailable) { + /* XXX: not implemented */ + pw_log_warn(NAME": only_from_unavailable is not implemented"); + } + + return module; + +out: + pw_properties_free(props); + if (blocklist) { + regfree(blocklist); + free(blocklist); + } + errno = -res; + + return NULL; +} diff --git a/src/modules/module-protocol-pulse/modules/registry.h b/src/modules/module-protocol-pulse/modules/registry.h index 6825a1a11..3b79455e9 100644 --- a/src/modules/module-protocol-pulse/modules/registry.h +++ b/src/modules/module-protocol-pulse/modules/registry.h @@ -40,6 +40,7 @@ struct module *create_module_remap_source(struct impl *impl, const char *argumen struct module *create_module_tunnel_sink(struct impl *impl, const char *argument); struct module *create_module_tunnel_source(struct impl *impl, const char *argument); struct module *create_module_simple_protocol_tcp(struct impl *impl, const char *argument); +struct module *create_module_switch_on_connect(struct impl *impl, const char *argument); struct module *create_module_pipe_source(struct impl *impl, const char *argument); struct module *create_module_pipe_sink(struct impl *impl, const char *argument); struct module *create_module_zeroconf_discover(struct impl *impl, const char *argument);