diff --git a/doc/pipewire-modules.dox b/doc/pipewire-modules.dox index ae9028f6b..eeb848ed4 100644 --- a/doc/pipewire-modules.dox +++ b/doc/pipewire-modules.dox @@ -59,6 +59,7 @@ List of known modules: - \subpage page_module_echo_cancel - \subpage page_module_example_sink - \subpage page_module_example_source +- \subpage page_module_fallback_sink - \subpage page_module_filter_chain - \subpage page_module_link_factory - \subpage page_module_loopback diff --git a/po/POTFILES.in b/po/POTFILES.in index 9223c54a6..96c8261bd 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,6 +2,7 @@ src/daemon/pipewire.c src/daemon/pipewire.desktop.in src/modules/module-protocol-pulse/modules/module-tunnel-sink.c src/modules/module-protocol-pulse/modules/module-tunnel-source.c +src/modules/module-fallback-sink.c src/modules/module-pulse-tunnel.c src/modules/module-zeroconf-discover.c src/tools/pw-cat.c diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in index 1cea3d11e..e6cb7903c 100644 --- a/src/daemon/pipewire-pulse.conf.in +++ b/src/daemon/pipewire-pulse.conf.in @@ -68,6 +68,9 @@ context.modules = [ } } } + + # Pulseaudio shows a fallback null sink, if no other sinks are present + { name = libpipewire-module-fallback-sink, args = { sink.name = "auto_null" } } ] # Extra modules can be loaded here. Setup in default.pa can be moved here diff --git a/src/modules/meson.build b/src/modules/meson.build index 54612a8e2..52b7c9ab1 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -10,6 +10,7 @@ module_sources = [ 'module-echo-cancel.c', 'module-example-sink.c', 'module-example-source.c', + 'module-fallback-sink.c', 'module-filter-chain.c', 'module-link-factory.c', 'module-loopback.c', @@ -498,3 +499,12 @@ pipewire_module_x11_bell = shared_library('pipewire-module-x11-bell', ) endif summary({'x11-bell': build_module_x11_bell}, bool_yn: true, section: 'Optional Modules') + +pipewire_module_fallback_sink = shared_library('pipewire-module-fallback-sink', + [ 'module-fallback-sink.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep], +) diff --git a/src/modules/module-fallback-sink.c b/src/modules/module-fallback-sink.c new file mode 100644 index 000000000..9a277d127 --- /dev/null +++ b/src/modules/module-fallback-sink.c @@ -0,0 +1,473 @@ +/* PipeWire + * + * Copyright © 2021 Wim Taymans + * + * 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 +#include +#include + +#include "config.h" + +#include +#include +#include + +#include +#include + +/** \page page_module_fallback_sink PipeWire Module: Fallback Sink + * + * Fallback sink, which appear dynamically when no other sinks are + * present. This is only useful for Pulseaudio compatibility. + */ + +#define NAME "fallback-sink" + +#define DEFAULT_SINK_NAME "auto_null" +#define DEFAULT_SINK_DESCRIPTION _("Dummy Output") + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define MODULE_USAGE ("[ sink.name= ] " \ + "[ sink.description= ] ") + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen " }, + { PW_KEY_MODULE_DESCRIPTION, "Dynamically appearing fallback sink" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct bitmap { + uint8_t *data; + size_t size; + size_t items; +}; + +struct impl { + struct pw_context *context; + + struct pw_impl_module *module; + struct spa_hook module_listener; + + struct pw_core *core; + struct pw_registry *registry; + struct pw_proxy *sink; + + struct spa_hook core_listener; + struct spa_hook core_proxy_listener; + struct spa_hook registry_listener; + struct spa_hook sink_listener; + + struct pw_properties *properties; + + struct bitmap sink_ids; + struct bitmap fallback_sink_ids; + + int check_seq; + + unsigned int do_disconnect:1; + unsigned int scheduled:1; +}; + +static int bitmap_add(struct bitmap *map, uint32_t i) +{ + const uint32_t pos = (i >> 3); + const uint8_t mask = 1 << (i & 0x7); + + if (pos >= map->size) { + size_t new_size = map->size + pos + 16; + void *p; + + p = realloc(map->data, new_size); + if (!p) + return -errno; + + memset((uint8_t*)p + map->size, 0, new_size - map->size); + map->data = p; + map->size = new_size; + } + + if (map->data[pos] & mask) + return 1; + + map->data[pos] |= mask; + ++map->items; + + return 0; +} + +static bool bitmap_remove(struct bitmap *map, uint32_t i) +{ + const uint32_t pos = (i >> 3); + const uint8_t mask = 1 << (i & 0x7); + + if (pos >= map->size) + return false; + + if (!(map->data[pos] & mask)) + return false; + + map->data[pos] &= ~mask; + --map->items; + + return true; +} + +static void bitmap_free(struct bitmap *map) +{ + free(map->data); + spa_zero(*map); +} + +static int add_id(struct bitmap *map, uint32_t id) +{ + int res; + + if (id == SPA_ID_INVALID) + return -EINVAL; + + if ((res = bitmap_add(map, id)) < 0) + pw_log_error("%s", spa_strerror(res)); + + return res; +} + +static void reschedule_check(struct impl *impl) +{ + if (!impl->scheduled) + return; + + impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq); +} + +static void schedule_check(struct impl *impl) +{ + if (impl->scheduled) + return; + + impl->scheduled = true; + impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq); +} + +static void sink_proxy_removed(void *data) +{ + struct impl *impl = data; + + pw_proxy_destroy(impl->sink); +} + +static void sink_proxy_bound(void *data, uint32_t id) +{ + struct impl *impl = data; + + add_id(&impl->sink_ids, id); + add_id(&impl->fallback_sink_ids, id); + + reschedule_check(impl); + schedule_check(impl); +} + +static void sink_proxy_destroy(void *data) +{ + struct impl *impl = data; + + pw_log_debug("fallback dummy sink destroyed"); + + spa_hook_remove(&impl->sink_listener); + impl->sink = NULL; +} + +static const struct pw_proxy_events sink_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = sink_proxy_removed, + .bound = sink_proxy_bound, + .destroy = sink_proxy_destroy, +}; + +static int sink_create(struct impl *impl) +{ + if (impl->sink) + return 0; + + pw_log_info("creating fallback dummy sink"); + + impl->sink = pw_core_create_object(impl->core, + "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, + impl->properties ? &impl->properties->dict : NULL, 0); + if (impl->sink == NULL) + return -errno; + + pw_proxy_add_listener(impl->sink, &impl->sink_listener, &sink_proxy_events, impl); + + return 0; +} + +static void sink_destroy(struct impl *impl) +{ + if (!impl->sink) + return; + + pw_log_info("removing fallback dummy sink"); + pw_proxy_destroy(impl->sink); +} + +static void check_sinks(struct impl *impl) +{ + int res; + + pw_log_debug("seeing %zu sink(s), %zu fallback sink(s)", + impl->sink_ids.items, impl->fallback_sink_ids.items); + + if (impl->sink_ids.items > impl->fallback_sink_ids.items) { + sink_destroy(impl); + } else { + if ((res = sink_create(impl)) < 0) + pw_log_error("error creating sink: %s", spa_strerror(res)); + } +} + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + struct impl *impl = data; + const char *str; + + reschedule_check(impl); + + if (!props) + return; + + if (!spa_streq(type, PW_TYPE_INTERFACE_Node)) + return; + + str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (!(spa_streq(str, "Audio/Sink") || spa_streq(str, "Audio/Sink/Virtual"))) + return; + + add_id(&impl->sink_ids, id); + schedule_check(impl); +} + +static void registry_event_global_remove(void *data, uint32_t id) +{ + struct impl *impl = data; + + reschedule_check(impl); + + bitmap_remove(&impl->fallback_sink_ids, id); + if (bitmap_remove(&impl->sink_ids, id)) + schedule_check(impl); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void core_done(void *data, uint32_t id, int seq) +{ + struct impl *impl = data; + + if (seq == impl->check_seq) { + impl->scheduled = false; + check_sinks(impl); + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .done = core_done, +}; + +static void core_proxy_removed(void *data) +{ + struct impl *impl = data; + + if (impl->registry) { + spa_hook_remove(&impl->registry_listener); + pw_proxy_destroy((struct pw_proxy*)impl->registry); + impl->registry = NULL; + } +} + +static void core_proxy_destroy(void *data) +{ + struct impl *impl = data; + + spa_hook_remove(&impl->core_listener); + spa_hook_remove(&impl->core_proxy_listener); + impl->core = NULL; +} + +static const struct pw_proxy_events core_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .destroy = core_proxy_destroy, + .removed = core_proxy_removed, +}; + +static void impl_destroy(struct impl *impl) +{ + sink_destroy(impl); + + if (impl->registry) { + spa_hook_remove(&impl->registry_listener); + pw_proxy_destroy((struct pw_proxy*)impl->registry); + impl->registry = NULL; + } + + if (impl->core) { + spa_hook_remove(&impl->core_listener); + spa_hook_remove(&impl->core_proxy_listener); + if (impl->do_disconnect) + pw_core_disconnect(impl->core); + impl->core = NULL; + } + + if (impl->properties) { + pw_properties_free(impl->properties); + impl->properties = NULL; + } + + bitmap_free(&impl->sink_ids); + bitmap_free(&impl->fallback_sink_ids); + + free(impl); +} + +static void module_destroy(void *data) +{ + struct impl *impl = data; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *props = NULL; + struct impl *impl = NULL; + const char *str; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + goto error_errno; + + pw_log_debug("module %p: new %s", impl, args); + + if (args == NULL) + args = ""; + + impl->module = module; + impl->context = context; + + props = pw_properties_new_string(args); + if (props == NULL) + goto error_errno; + + impl->properties = pw_properties_new(NULL, NULL); + if (impl->properties == NULL) + goto error_errno; + + if ((str = pw_properties_get(props, "sink.name")) == NULL) + str = DEFAULT_SINK_NAME; + pw_properties_set(impl->properties, PW_KEY_NODE_NAME, str); + + if ((str = pw_properties_get(props, "sink.description")) == NULL) + str = DEFAULT_SINK_DESCRIPTION; + pw_properties_set(impl->properties, PW_KEY_NODE_DESCRIPTION, str); + + pw_properties_setf(impl->properties, SPA_KEY_AUDIO_RATE, "%u", 48000); + pw_properties_setf(impl->properties, SPA_KEY_AUDIO_CHANNELS, "%u", 2); + pw_properties_set(impl->properties, SPA_KEY_AUDIO_POSITION, "FL,FR"); + + pw_properties_set(impl->properties, PW_KEY_MEDIA_CLASS, "Audio/Sink"); + pw_properties_set(impl->properties, PW_KEY_FACTORY_NAME, "support.null-audio-sink"); + pw_properties_set(impl->properties, PW_KEY_NODE_VIRTUAL, "true"); + pw_properties_set(impl->properties, "monitor.channel-volumes", "true"); + + impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); + if (impl->core == NULL) { + str = pw_properties_get(props, PW_KEY_REMOTE_NAME); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, &core_proxy_events, + impl); + + pw_core_add_listener(impl->core, &impl->core_listener, &core_events, impl); + + impl->registry = pw_core_get_registry(impl->core, + PW_VERSION_REGISTRY, 0); + if (impl->registry == NULL) + goto error_errno; + + pw_registry_add_listener(impl->registry, + &impl->registry_listener, + ®istry_events, impl); + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); + + schedule_check(impl); + + pw_properties_free(props); + return 0; + +error_errno: + res = -errno; +error: + if (props) + pw_properties_free(props); + if (impl) + impl_destroy(impl); + return res; +}