pipewire/src/modules/module-raop-discover.c

495 lines
12 KiB
C
Raw Normal View History

/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
/* SPDX-License-Identifier: MIT */
2021-11-11 11:42:16 +01:00
#include "config.h"
2021-11-11 11:42:16 +01:00
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <spa/utils/result.h>
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <pipewire/impl.h>
#include <pipewire/i18n.h>
#include "zeroconf-utils/zeroconf.h"
2021-11-11 11:42:16 +01:00
#include "module-protocol-pulse/format.h"
/** \page page_module_raop_discover RAOP Discover
2022-04-22 17:29:40 +02:00
*
* Automatically creates RAOP (Airplay) sink devices based on zeroconf
* information.
*
* This module will load module-raop-sink for each announced stream that matches
* the rule with the create-stream action.
*
* If no stream.rules are given, it will create a sink for all announced
* streams.
2022-04-22 17:29:40 +02:00
*
* ## Module Name
*
* `libpipewire-module-raop-discover`
*
2022-04-22 17:29:40 +02:00
* ## Module Options
*
* Options specific to the behavior of this module
*
* - `raop.discover-local` = allow discovery of local services as well.
* false by default.
* - `raop.latency.ms` = latency for all streams in microseconds. This
* can be overwritten in the stream rules.
* - `stream.rules` = <rules>: match rules, use create-stream actions. See
* \ref page_module_raop_sink for module properties.
2022-04-22 17:29:40 +02:00
*
* ## Example configuration
*
*\code{.unparsed}
* # ~/.config/pipewire/pipewire.conf.d/my-raop-discover.conf
*
2022-04-22 17:29:40 +02:00
* context.modules = [
* { name = libpipewire-module-raop-discover
* args = {
* #raop.discover-local = false;
* #raop.latency.ms = 1000
* stream.rules = [
* { matches = [
* { raop.ip = "~.*"
* #raop.port = 1000
* #raop.name = ""
* #raop.hostname = ""
* #raop.domain = ""
* #raop.device = ""
* #raop.transport = "udp" | "tcp"
* #raop.encryption.type = "none" | "RSA" | "auth_setup" | "fp_sap25"
* #raop.audio.codec = "PCM" | "ALAC" | "AAC" | "AAC-ELD"
* #audio.channels = 2
* #audio.format = "S16" | "S24" | "S32"
* #audio.rate = 44100
* #device.model = ""
* }
* ]
* actions = {
* create-stream = {
* #raop.password = ""
* stream.props = {
* #target.object = ""
* #media.class = "Audio/Sink"
* }
* }
* }
* }
* ]
* }
2022-04-22 17:29:40 +02:00
* }
* ]
*\endcode
*
* ## See also
*
* \ref page_module_raop_sink
2021-11-11 11:42:16 +01:00
*/
#define NAME "raop-discover"
2021-11-11 11:42:16 +01:00
PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
#define PW_LOG_TOPIC_DEFAULT mod_topic
#define MODULE_USAGE "( stream.rules=<rules>, use create-stream actions )"
#define DEFAULT_CREATE_RULES \
"[ { matches = [ { raop.ip = \"~.*\" } ] actions = { create-stream = { } } } ] "
2021-11-11 11:42:16 +01:00
static const struct spa_dict_item module_props[] = {
{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
{ PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" },
{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
};
#define SERVICE_TYPE_SINK "_raop._tcp"
struct impl {
struct pw_context *context;
bool discover_local;
2021-11-11 11:42:16 +01:00
struct pw_impl_module *module;
struct spa_hook module_listener;
struct pw_properties *properties;
struct pw_zeroconf *zeroconf;
struct spa_hook zeroconf_listener;
2021-11-11 11:42:16 +01:00
struct spa_list tunnel_list;
};
struct tunnel {
struct spa_list link;
char *name;
2021-11-11 11:42:16 +01:00
struct pw_impl_module *module;
struct spa_hook module_listener;
};
static struct tunnel *tunnel_new(struct impl *impl, const char *name)
2021-11-11 11:42:16 +01:00
{
struct tunnel *t;
t = calloc(1, sizeof(*t));
if (t == NULL)
return NULL;
t->name = strdup(name);
2021-11-11 11:42:16 +01:00
spa_list_append(&impl->tunnel_list, &t->link);
return t;
}
static struct tunnel *find_tunnel(struct impl *impl, const char *name)
2021-11-11 11:42:16 +01:00
{
struct tunnel *t;
spa_list_for_each(t, &impl->tunnel_list, link) {
if (spa_streq(t->name, name))
2021-11-11 11:42:16 +01:00
return t;
}
return NULL;
}
static void tunnel_free(struct tunnel *t)
2021-11-11 11:42:16 +01:00
{
spa_list_remove(&t->link);
if (t->module)
pw_impl_module_destroy(t->module);
free(t->name);
free(t);
2021-11-11 11:42:16 +01:00
}
static void impl_free(struct impl *impl)
{
struct tunnel *t;
spa_list_consume(t, &impl->tunnel_list, link)
tunnel_free(t);
if (impl->zeroconf)
pw_zeroconf_destroy(impl->zeroconf);
2021-11-11 11:42:16 +01:00
pw_properties_free(impl->properties);
free(impl);
}
static void module_destroy(void *data)
{
struct impl *impl = data;
spa_hook_remove(&impl->module_listener);
impl_free(impl);
}
static const struct pw_impl_module_events module_events = {
PW_VERSION_IMPL_MODULE_EVENTS,
.destroy = module_destroy,
};
static bool str_in_list(const char *haystack, const char *delimiters, const char *needle)
{
const char *s, *state = NULL;
size_t len;
while ((s = pw_split_walk(haystack, delimiters, &len, &state))) {
if (spa_strneq(needle, s, len))
return true;
}
return false;
}
static void submodule_destroy(void *data)
{
struct tunnel *t = data;
spa_hook_remove(&t->module_listener);
t->module = NULL;
2021-11-11 11:42:16 +01:00
}
static const struct pw_impl_module_events submodule_events = {
PW_VERSION_IMPL_MODULE_EVENTS,
.destroy = submodule_destroy,
};
struct match_info {
struct impl *impl;
struct pw_properties *props;
struct tunnel *tunnel;
bool matched;
};
static int create_stream(struct impl *impl, struct pw_properties *props,
struct tunnel *t)
{
FILE *f;
char *args;
size_t size;
int res = 0;
struct pw_impl_module *mod;
if ((f = open_memstream(&args, &size)) == NULL) {
res = -errno;
pw_log_error("Can't open memstream: %m");
goto done;
}
fprintf(f, "{");
pw_properties_serialize_dict(f, &props->dict, 0);
fprintf(f, "}");
fclose(f);
pw_log_info("loading module args:'%s'", args);
mod = pw_context_load_module(impl->context,
"libpipewire-module-raop-sink",
args, NULL);
free(args);
if (mod == NULL) {
res = -errno;
pw_log_error("Can't load module: %m");
goto done;
}
pw_impl_module_add_listener(mod, &t->module_listener, &submodule_events, t);
t->module = mod;
done:
return res;
}
static int rule_matched(void *data, const char *location, const char *action,
const char *str, size_t len)
{
struct match_info *i = data;
int res = 0;
i->matched = true;
if (spa_streq(action, "create-stream")) {
pw_properties_update_string(i->props, str, len);
create_stream(i->impl, i->props, i->tunnel);
}
return res;
}
static void pw_properties_from_zeroconf(const char *key, const char *value,
struct pw_properties *props)
2021-11-11 11:42:16 +01:00
{
if (spa_streq(key, "zeroconf.ifindex")) {
pw_properties_set(props, "raop.ifindex", value);
}
else if (spa_streq(key, "zeroconf.address")) {
pw_properties_set(props, "raop.ip", value);
}
else if (spa_streq(key, "zeroconf.port")) {
pw_properties_set(props, "raop.port", value);
}
else if (spa_streq(key, "zeroconf.name")) {
pw_properties_set(props, "raop.name", value);
}
else if (spa_streq(key, "zeroconf.hostname")) {
pw_properties_set(props, "raop.hostname", value);
}
else if (spa_streq(key, "zeroconf.domain")) {
pw_properties_set(props, "raop.domain", value);
}
else if (spa_streq(key, "device")) {
pw_properties_set(props, "raop.device", value);
2021-11-11 11:42:16 +01:00
}
else if (spa_streq(key, "tp")) {
/* transport protocol, "UDP", "TCP", "UDP,TCP" */
if (str_in_list(value, ",", "UDP"))
value = "udp";
else if (str_in_list(value, ",", "TCP"))
value = "tcp";
pw_properties_set(props, "raop.transport", value);
} else if (spa_streq(key, "et")) {
/* RAOP encryption types:
* 0 = none,
* 1 = RSA,
* 3 = FairPlay,
* 4 = MFiSAP (/auth-setup),
* 5 = FairPlay SAPv2.5 */
if (str_in_list(value, ",", "5"))
value = "fp_sap25";
else if (str_in_list(value, ",", "4"))
value = "auth_setup";
else if (str_in_list(value, ",", "1"))
value = "RSA";
else
value = "none";
pw_properties_set(props, "raop.encryption.type", value);
} else if (spa_streq(key, "cn")) {
/* Supported audio codecs:
* 0 = PCM,
* 1 = ALAC,
* 2 = AAC,
* 3 = AAC ELD. */
if (str_in_list(value, ",", "0"))
value = "PCM";
else if (str_in_list(value, ",", "1"))
value = "ALAC";
else if (str_in_list(value, ",", "2"))
value = "AAC";
else if (str_in_list(value, ",", "3"))
value = "AAC-ELD";
else
value = "unknown";
pw_properties_set(props, "raop.audio.codec", value);
} else if (spa_streq(key, "ch")) {
/* Number of channels */
pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value);
} else if (spa_streq(key, "ss")) {
/* Sample size */
if (spa_streq(value, "16"))
value = "S16";
else if (spa_streq(value, "24"))
value = "S24";
else if (spa_streq(value, "32"))
value = "S32";
else
value = "UNKNOWN";
pw_properties_set(props, PW_KEY_AUDIO_FORMAT, value);
} else if (spa_streq(key, "sr")) {
/* Sample rate */
pw_properties_set(props, PW_KEY_AUDIO_RATE, value);
} else if (spa_streq(key, "am")) {
/* Device model */
pw_properties_set(props, "device.model", value);
}
}
2026-02-27 16:06:33 +01:00
static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info)
{
struct impl *impl = data;
const char *name, *str;
struct tunnel *t;
const struct spa_dict_item *it;
struct pw_properties *props = NULL;
name = spa_dict_lookup(info, "zeroconf.name");
t = find_tunnel(impl, name);
if (t == NULL) {
if ((t = tunnel_new(impl, name)) == NULL) {
pw_log_error("Can't make tunnel: %m");
goto done;
}
}
if (t->module != NULL) {
pw_log_info("found duplicate mdns entry for %s on IP %s - "
"skipping tunnel creation", name,
spa_dict_lookup(info, "zeroconf.address"));
goto done;
}
2021-11-11 11:42:16 +01:00
if ((props = pw_properties_new(NULL, NULL)) == NULL) {
2021-11-11 11:42:16 +01:00
pw_log_error("Can't allocate properties: %m");
goto done;
}
spa_dict_for_each(it, info)
pw_properties_from_zeroconf(it->key, it->value, props);
2021-11-11 11:42:16 +01:00
if ((str = pw_properties_get(impl->properties, "raop.latency.ms")) != NULL)
pw_properties_set(props, "raop.latency.ms", str);
if ((str = pw_properties_get(impl->properties, "stream.rules")) == NULL)
str = DEFAULT_CREATE_RULES;
if (str != NULL) {
struct match_info minfo = {
.impl = impl,
.props = props,
.tunnel = t,
};
pw_conf_match_rules(str, strlen(str), NAME, &props->dict,
rule_matched, &minfo);
if (!minfo.matched)
pw_log_info("unmatched service found %s", str);
2021-11-11 11:42:16 +01:00
}
done:
pw_properties_free(props);
2021-11-11 11:42:16 +01:00
}
2026-02-27 16:06:33 +01:00
static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info)
2021-11-11 11:42:16 +01:00
{
struct impl *impl = data;
const char *name;
2021-11-11 11:42:16 +01:00
struct tunnel *t;
name = spa_dict_lookup(info, "zeroconf.name");
2021-11-11 11:42:16 +01:00
if ((t = find_tunnel(impl, name)) == NULL)
return;
2021-11-11 11:42:16 +01:00
tunnel_free(t);
2021-11-11 11:42:16 +01:00
}
static const struct pw_zeroconf_events zeroconf_events = {
PW_VERSION_ZEROCONF_EVENTS,
.added = on_zeroconf_added,
.removed = on_zeroconf_removed,
};
2021-11-11 11:42:16 +01:00
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;
const char *local;
2021-11-11 11:42:16 +01:00
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 = "";
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->properties = props;
if ((local = pw_properties_get(impl->properties, "raop.discover-local")) == NULL)
local = "false";
pw_properties_set(impl->properties, "zeroconf.discover-local", local);
2021-11-11 11:42:16 +01:00
pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
if ((impl->zeroconf = pw_zeroconf_new(context, &props->dict)) == NULL) {
pw_log_error("can't create zeroconf: %m");
goto error_errno;
}
pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener,
&zeroconf_events, impl);
2021-11-11 11:42:16 +01:00
pw_zeroconf_set_browse(impl->zeroconf, NULL,
&SPA_DICT_ITEMS(
SPA_DICT_ITEM("zeroconf.service", SERVICE_TYPE_SINK)));
2021-11-11 11:42:16 +01:00
return 0;
error_errno:
res = -errno;
if (impl)
impl_free(impl);
return res;
}