diff --git a/meson.build b/meson.build index 03a9d174f..60dc563b2 100644 --- a/meson.build +++ b/meson.build @@ -311,6 +311,7 @@ sdl_dep = dependency('sdl2', required : get_option('sdl2')) ncurses_dep = dependency('ncursesw', required : false) sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile')) pulseaudio_dep = dependency('libpulse', required : get_option('libpulse')) +avahi_dep = dependency('avahi-client', required : get_option('avahi')) gst_option = get_option('gstreamer') gst_deps_def = { diff --git a/meson_options.txt b/meson_options.txt index 66a50780d..9bc33fcd8 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -187,3 +187,7 @@ option('libpulse', description: 'Enable code that depends on libpulse', type: 'feature', value: 'auto') +option('avahi', + description: 'Enable code that depends on avahi', + type: 'feature', + value: 'auto') diff --git a/src/modules/meson.build b/src/modules/meson.build index f58443187..4bfff7610 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -265,3 +265,16 @@ pipewire_module_session_manager = shared_library('pipewire-module-session-manage install_rpath: modules_install_dir, dependencies : [mathlib, dl_lib, pipewire_dep], ) + +if avahi_dep.found() +pipewire_module_zeroconf_discover = shared_library('pipewire-module-zeroconf-discover', + [ 'module-zeroconf-discover.c', + 'module-zeroconf-discover/avahi-poll.c' ], + c_args : pipewire_module_c_args, + include_directories : [configinc, spa_inc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep], +) +endif diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c new file mode 100644 index 000000000..17d9c03d0 --- /dev/null +++ b/src/modules/module-zeroconf-discover.c @@ -0,0 +1,477 @@ +/* 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 + +#include +#include +#include + +#include "module-zeroconf-discover/avahi-poll.h" + +#define NAME "zeroconf-discover" + +#define MODULE_USAGE " " + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +#define SERVICE_TYPE_SINK "_pulse-sink._tcp" +#define SERVICE_TYPE_SOURCE "_non-monitor._sub._pulse-source._tcp" + + +struct impl { + struct pw_context *context; + + struct pw_impl_module *module; + struct spa_hook module_listener; + struct pw_work_queue *work; + + struct pw_properties *properties; + + AvahiPoll *avahi_poll; + AvahiClient *client; + AvahiServiceBrowser *sink_browser; + AvahiServiceBrowser *source_browser; + + struct spa_list tunnel_list; + + unsigned int unloading:1; +}; + +struct tunnel_info { + AvahiIfIndex interface; + AvahiProtocol protocol; + const char *name; + const char *type; + const char *domain; +}; + +#define TUNNEL_INFO(...) (struct tunnel_info){ __VA_ARGS__ } + +struct tunnel { + struct spa_list link; + struct tunnel_info info; + struct pw_impl_module *module; + struct spa_hook module_listener; +}; + +static int start_client(struct impl *impl); + +static void do_unload_module(void *obj, void *data, int res, uint32_t id) +{ + struct impl *impl = data; + pw_impl_module_destroy(impl->module); +} + +static void unload_module(struct impl *impl) +{ + if (!impl->unloading) { + impl->unloading = true; + pw_work_queue_add(impl->work, impl, 0, do_unload_module, impl); + } +} + +static void module_destroy(void *data) +{ + struct impl *impl = data; + + spa_hook_remove(&impl->module_listener); + + if (impl->properties) + pw_properties_free(impl->properties); + + free(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info) +{ + struct tunnel *t; + + t = calloc(1, sizeof(*t)); + if (t == NULL) + return NULL; + + t->info.interface = info->interface; + t->info.protocol = info->protocol; + t->info.name = strdup(info->name); + t->info.type = strdup(info->type); + t->info.domain = strdup(info->domain); + spa_list_append(&impl->tunnel_list, &t->link); + + return t; +} + +static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *info) +{ + struct tunnel *t; + spa_list_for_each(t, &impl->tunnel_list, link) { + if (t->info.interface == info->interface && + t->info.protocol == info->protocol && + strcmp(t->info.name, info->name) == 0 && + strcmp(t->info.type, info->type) == 0 && + strcmp(t->info.domain, info->domain) == 0) + return t; + } + return NULL; +} + +static void free_tunnel(struct tunnel *t) +{ + spa_list_remove(&t->link); + if (t->module) + pw_impl_module_destroy(t->module); + free((char*)t->info.name); + free((char*)t->info.type); + free((char*)t->info.domain); + free(t); +} + +static void serialize_dict(FILE *f, const struct spa_dict *dict) +{ + const struct spa_dict_item *it; + spa_dict_for_each(it, dict) { + size_t len = it->value ? strlen(it->value) : 0; + fprintf(f, " \"%s\" = ", it->key); + if (it->value == NULL) { + fprintf(f, "null"); + } else if (spa_json_is_null(it->value, len) || + spa_json_is_float(it->value, len) || + spa_json_is_bool(it->value, len) || + spa_json_is_container(it->value, len)) { + fprintf(f, "%s", it->value); + } else { + size_t size = (len+1) * 4; + char str[size]; + spa_json_encode_string(str, size, it->value); + fprintf(f, "%s", str); + } + } +} + +static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiResolverEvent event, const char *name, const char *type, const char *domain, + const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt, + AvahiLookupResultFlags flags, void *userdata) +{ + struct impl *impl = userdata; + struct tunnel *t; + struct tunnel_info tinfo; + const char *str, *device; + char if_suffix[16] = ""; + char at[AVAHI_ADDRESS_STR_MAX]; + AvahiStringList *l; + FILE *f; + char *args; + size_t size; + struct pw_impl_module *mod; + struct pw_properties *props = NULL; + + if (event != AVAHI_RESOLVER_FOUND) { + pw_log_error("Resolving of '%s' failed: %s", name, + avahi_strerror(avahi_client_errno(impl->client))); + return; + } + + props = pw_properties_new(NULL, NULL); + if (props == NULL) { + pw_log_error("Can't allocate properties: %m"); + return; + } + + tinfo = TUNNEL_INFO(.interface = interface, + .protocol = protocol, + .name = name, + .type = type, + .domain = domain); + + for (l = txt; l; l = l->next) { + char *key, *value; + + if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) + break; + + if (strcmp(key, "device") == 0) { + pw_properties_set(props, "node.target", value); + } + else if (strcmp(key, "rate") == 0) { + pw_properties_setf(props, "audio.rate", "%u", atoi(value)); + } + else if (strcmp(key, "channels") == 0) { + pw_properties_setf(props, "audio.channels", "%u", atoi(value)); + } + else if (strcmp(key, "format") == 0) { + pw_properties_set(props, "audio.format", value); + } + else if (strcmp(key, "icon-name") == 0) { + pw_properties_set(props, "device.icon-name", value); + } + else if (strcmp(key, "channel_map") == 0) { + } + avahi_free(key); + avahi_free(value); + } + + if ((device = pw_properties_get(props, "node.target")) != NULL) + pw_properties_setf(props, "node.name", + "tunnel.%s.%s", host_name, device); + else + pw_properties_setf(props, "node.name", + "tunnel.%s", host_name); + + + str = strstr(type, "sink") ? "playback" : "capture"; + pw_properties_set(props, "tunnel.mode", str); + + if (a->proto == AVAHI_PROTO_INET6 && + a->data.ipv6.address[0] == 0xfe && + (a->data.ipv6.address[1] & 0xc0) == 0x80) + snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); + + pw_properties_setf(props, "pulse.server.address", "[%s%s]:%u", + avahi_address_snprint(at, sizeof(at), a), + if_suffix, port); + + if ((str = pw_properties_get(props, "pulse.server.address")) != NULL) + pw_properties_setf(props, "node.description", + _("Tunnel to %s/%s"), str, device); + + f = open_memstream(&args, &size); + fprintf(f, "{"); + serialize_dict(f, &props->dict); + fprintf(f, " stream.props = {"); + fprintf(f, " }"); + fprintf(f, "}"); + fclose(f); + + pw_properties_free(props); + + mod = pw_context_load_module(impl->context, + "libpipewire-module-pulse-tunnel", + args, NULL); + free(args); + + if (mod == NULL) { + pw_log_error("Can't load module: %m"); + return; + } + + t = make_tunnel(impl, &tinfo); + if (t == NULL) + return; + + t->module = mod; +} + + +static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiBrowserEvent event, const char *name, const char *type, const char *domain, + AvahiLookupResultFlags flags, void *userdata) +{ + struct impl *impl = userdata; + struct tunnel_info info; + struct tunnel *t; + + if (flags & AVAHI_LOOKUP_RESULT_LOCAL) + return; + + info = TUNNEL_INFO(.interface = interface, + .protocol = protocol, + .name = name, + .type = type, + .domain = domain); + + t = find_tunnel(impl, &info); + + switch (event) { + case AVAHI_BROWSER_NEW: + if (t != NULL) + return; + if (!(avahi_service_resolver_new(impl->client, + interface, protocol, + name, type, domain, + AVAHI_PROTO_UNSPEC, 0, + resolver_cb, impl))) + pw_log_error("can't make service resolver: %s", + avahi_strerror(avahi_client_errno(impl->client))); + break; + case AVAHI_BROWSER_REMOVE: + if (t == NULL) + return; + free_tunnel(t); + break; + default: + break; + } +} + + +static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type) +{ + struct AvahiServiceBrowser *s; + + s = avahi_service_browser_new(impl->client, + AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, + service_type, NULL, 0, + browser_cb, impl); + if (s == NULL) { + pw_log_error("can't make browser for %s: %s", service_type, + avahi_strerror(avahi_client_errno(impl->client))); + } + return s; +} + +static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) +{ + struct impl *impl = userdata; + + impl->client = c; + + switch (state) { + case AVAHI_CLIENT_S_REGISTERING: + case AVAHI_CLIENT_S_RUNNING: + case AVAHI_CLIENT_S_COLLISION: + if (impl->sink_browser == NULL) + impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK); + if (impl->sink_browser == NULL) + goto error; + + if (impl->source_browser == NULL) + impl->source_browser = make_browser(impl, SERVICE_TYPE_SOURCE); + if (impl->source_browser == NULL) + goto error; + + break; + case AVAHI_CLIENT_FAILURE: + if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED) + start_client(impl); + + SPA_FALLTHROUGH; + case AVAHI_CLIENT_CONNECTING: + if (impl->sink_browser) { + avahi_service_browser_free(impl->sink_browser); + impl->sink_browser = NULL; + } + if (impl->source_browser) { + avahi_service_browser_free(impl->source_browser); + impl->source_browser = NULL; + } + break; + default: + break; + } + return; +error: + unload_module(impl); +} + +static int start_client(struct impl *impl) +{ + int res; + if ((impl->client = avahi_client_new(impl->avahi_poll, + AVAHI_CLIENT_NO_FAIL, + client_callback, impl, + &res)) == NULL) { + pw_log_error("can't create client: %s", avahi_strerror(res)); + unload_module(impl); + return -EIO; + } + return 0; +} + +static int start_avahi(struct impl *impl) +{ + struct pw_loop *loop; + + loop = pw_context_get_main_loop(impl->context); + impl->avahi_poll = pw_avahi_poll_new(loop); + + return start_client(impl);; +} + +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; + struct impl *impl; + int res; + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + goto error_errno; + + pw_log_debug("module %p: new %s", impl, args); + + if (args) + args = ""; + + props = pw_properties_new_string(args); + if (props == NULL) + goto error_errno; + + spa_list_init(&impl->tunnel_list); + + impl->module = module; + impl->context = context; + impl->work = pw_context_get_work_queue(context); + impl->properties = props; + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); + + start_avahi(impl); + + return 0; + +error_errno: + res = -errno; + free(impl); + return res; +} diff --git a/src/modules/module-zeroconf-discover/avahi-poll.c b/src/modules/module-zeroconf-discover/avahi-poll.c new file mode 100644 index 000000000..033d16a9f --- /dev/null +++ b/src/modules/module-zeroconf-discover/avahi-poll.c @@ -0,0 +1,191 @@ +/* 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 "avahi-poll.h" + +struct impl { + AvahiPoll api; + struct pw_loop *loop; +}; + +struct AvahiWatch { + struct impl *impl; + struct spa_source *source; + AvahiWatchEvent events; + AvahiWatchCallback callback; + void *userdata; +}; + +struct AvahiTimeout { + struct impl *impl; + struct spa_source *source; + AvahiTimeoutCallback callback; + void *userdata; +}; + +static AvahiWatchEvent from_pw_events(uint32_t mask) +{ + return (mask & SPA_IO_IN ? AVAHI_WATCH_IN : 0) | + (mask & SPA_IO_OUT ? AVAHI_WATCH_OUT : 0) | + (mask & SPA_IO_ERR ? AVAHI_WATCH_ERR : 0) | + (mask & SPA_IO_HUP ? AVAHI_WATCH_HUP : 0); +} + +static uint32_t to_pw_events(AvahiWatchEvent e) { + return (e & AVAHI_WATCH_IN ? SPA_IO_IN : 0) | + (e & AVAHI_WATCH_OUT ? SPA_IO_OUT : 0) | + (e & AVAHI_WATCH_ERR ? SPA_IO_ERR : 0) | + (e & AVAHI_WATCH_HUP ? SPA_IO_HUP : 0); +} + +static void watch_callback(void *data, int fd, uint32_t mask) +{ + AvahiWatch *w = data; + + w->events = from_pw_events(mask); + w->callback(w, fd, w->events, w->userdata); + w->events = 0; +} + +static AvahiWatch* watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event, + AvahiWatchCallback callback, void *userdata) +{ + struct impl *impl = api->userdata; + AvahiWatch *w; + + w = calloc(1, sizeof(*w)); + if (w == NULL) + return NULL; + + w->impl = impl; + w->events = 0; + w->callback = callback; + w->userdata = userdata; + w->source = pw_loop_add_io(impl->loop, fd, to_pw_events(event), + false, watch_callback, w); + + return w; +} + +static void watch_update(AvahiWatch *w, AvahiWatchEvent event) +{ + struct impl *impl = w->impl; + pw_loop_update_io(impl->loop, w->source, to_pw_events(event)); +} + +static AvahiWatchEvent watch_get_events(AvahiWatch *w) +{ + return w->events; +} + +static void watch_free(AvahiWatch *w) +{ + struct impl *impl = w->impl; + pw_loop_destroy_source(impl->loop, w->source); + free(w); +} + +static void timeout_callback(void *data, uint64_t expirations) +{ + AvahiTimeout *w = data; + w->callback(w, w->userdata); +} + +static AvahiTimeout* timeout_new(const AvahiPoll *api, const struct timeval *tv, + AvahiTimeoutCallback callback, void *userdata) +{ + struct impl *impl = api->userdata; + struct timespec value; + AvahiTimeout *w; + + w = calloc(1, sizeof(*w)); + if (w == NULL) + return NULL; + + w->impl = impl; + w->callback = callback; + w->userdata = userdata; + w->source = pw_loop_add_timer(impl->loop, timeout_callback, w); + + if (tv != NULL) { + value.tv_sec = tv->tv_sec; + value.tv_nsec = tv->tv_usec * 1000UL; + pw_loop_update_timer(impl->loop, w->source, &value, NULL, true); + } + return w; +} + +static void timeout_update(AvahiTimeout *t, const struct timeval *tv) +{ + struct impl *impl = t->impl; + struct timespec value, *v = NULL; + + if (tv != NULL) { + value.tv_sec = tv->tv_sec; + value.tv_nsec = tv->tv_usec * 1000UL; + if (value.tv_sec == 0 && value.tv_nsec == 0) + value.tv_nsec = 1; + v = &value; + } + pw_loop_update_timer(impl->loop, t->source, v, NULL, true); +} + +static void timeout_free(AvahiTimeout *t) +{ + struct impl *impl = t->impl; + pw_loop_destroy_source(impl->loop, t->source); + free(t); +} + +static const AvahiPoll avahi_poll_api = { + .watch_new = watch_new, + .watch_update = watch_update, + .watch_get_events = watch_get_events, + .watch_free = watch_free, + .timeout_new = timeout_new, + .timeout_update = timeout_update, + .timeout_free = timeout_free, +}; + +AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop) +{ + struct impl *impl; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->loop = loop; + impl->api = avahi_poll_api; + impl->api.userdata = impl; + + return &impl->api; +} + +void pw_avahi_poll_free(AvahiPoll *p) +{ + free(p); +} diff --git a/src/modules/module-zeroconf-discover/avahi-poll.h b/src/modules/module-zeroconf-discover/avahi-poll.h new file mode 100644 index 000000000..04b785d8a --- /dev/null +++ b/src/modules/module-zeroconf-discover/avahi-poll.h @@ -0,0 +1,31 @@ +/* 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 + +AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop); + +void pw_avahi_poll_free(AvahiPoll *p);