module-raop: add match rules for discover

Useful for selecting only ip4 streams or for setting up the password.
This commit is contained in:
Wim Taymans 2023-03-15 17:23:41 +01:00
parent 9e56fae236
commit 8167e1b9be
2 changed files with 160 additions and 66 deletions

View file

@ -31,19 +31,56 @@
* Automatically creates RAOP (Airplay) sink devices based on zeroconf * Automatically creates RAOP (Airplay) sink devices based on zeroconf
* information. * information.
* *
* This module will load module-raop-sink for each discovered sink * This module will load module-raop-sink for each announced stream that matches
* with the right parameters. * the rule with the create-stream action.
*
* If no stream.rules are given, it will create a sink for all announced
* streams.
* *
* ## Module Options * ## Module Options
* *
* This module has no options. * Options specific to the behavior of this module
*
* - `stream.rules` = <rules>: match rules, use create-stream actions. See
* \ref page_module_raop_sink for module properties.
* *
* ## Example configuration * ## Example configuration
* *
*\code{.unparsed} *\code{.unparsed}
* context.modules = [ * context.modules = [
* { name = libpipewire-raop-discover * { name = libpipewire-raop-discover
* args = { } * args = {
* stream.rules = [
* { matches = [
* { raop.ip = "~.*"
* #raop.ip.version = 4 | 6
* #raop.ip.version = 4
* #raop.port = 1000
* #raop.name = ""
* #raop.hostname = ""
* #raop.domain = ""
* #raop.device = ""
* #raop.transport = "udp" | "tcp"
* #raop.encryption.type = "RSA" | "auth_setup" | "none"
* #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"
* }
* }
* }
* }
* ]
* }
* } * }
* ] * ]
*\endcode *\endcode
@ -58,7 +95,10 @@
PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
#define PW_LOG_TOPIC_DEFAULT mod_topic #define PW_LOG_TOPIC_DEFAULT mod_topic
#define MODULE_USAGE " " #define MODULE_USAGE "stream.rules=<rules>, use create-stream actions "
#define DEFAULT_CREATE_RULES \
"[ { matches = [ { raop.ip = \"~.*\" } ] actions = { create-stream = { } } } ] "
static const struct spa_dict_item module_props[] = { static const struct spa_dict_item module_props[] = {
{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" }, { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
@ -88,6 +128,7 @@ struct tunnel_info {
AvahiIfIndex interface; AvahiIfIndex interface;
AvahiProtocol protocol; AvahiProtocol protocol;
const char *name; const char *name;
const char *host_name;
const char *type; const char *type;
const char *domain; const char *domain;
}; };
@ -114,6 +155,7 @@ static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *i
t->info.interface = info->interface; t->info.interface = info->interface;
t->info.protocol = info->protocol; t->info.protocol = info->protocol;
t->info.name = strdup(info->name); t->info.name = strdup(info->name);
t->info.host_name = strdup(info->host_name);
t->info.type = strdup(info->type); t->info.type = strdup(info->type);
t->info.domain = strdup(info->domain); t->info.domain = strdup(info->domain);
spa_list_append(&impl->tunnel_list, &t->link); spa_list_append(&impl->tunnel_list, &t->link);
@ -255,6 +297,7 @@ static void submodule_destroy(void *data)
spa_hook_remove(&t->module_listener); spa_hook_remove(&t->module_listener);
free((char *) t->info.name); free((char *) t->info.name);
free((char *) t->info.host_name);
free((char *) t->info.type); free((char *) t->info.type);
free((char *) t->info.domain); free((char *) t->info.domain);
@ -266,20 +309,84 @@ static const struct pw_impl_module_events submodule_events = {
.destroy = submodule_destroy, .destroy = submodule_destroy,
}; };
struct match_info {
struct impl *impl;
struct pw_properties *props;
struct tunnel_info *tinfo;
bool matched;
};
static int create_stream(struct impl *impl, struct pw_properties *props,
struct tunnel_info *tinfo)
{
FILE *f;
char *args;
size_t size;
int res = 0;
struct pw_impl_module *mod;
struct tunnel *t;
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;
}
t = make_tunnel(impl, tinfo);
if (t == NULL) {
res = -errno;
pw_log_error("Can't make tunnel: %m");
pw_impl_module_destroy(mod);
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->tinfo);
}
return res;
}
static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
AvahiResolverEvent event, const char *name, const char *type, const char *domain, AvahiResolverEvent event, const char *name, const char *type, const char *domain,
const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt, const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
AvahiLookupResultFlags flags, void *userdata) AvahiLookupResultFlags flags, void *userdata)
{ {
struct impl *impl = userdata; struct impl *impl = userdata;
struct tunnel *t;
struct tunnel_info tinfo; struct tunnel_info tinfo;
const char *str; const char *str;
AvahiStringList *l; AvahiStringList *l;
FILE *f;
char *args;
size_t size;
struct pw_impl_module *mod;
struct pw_properties *props = NULL; struct pw_properties *props = NULL;
char at[AVAHI_ADDRESS_STR_MAX]; char at[AVAHI_ADDRESS_STR_MAX];
int ipv; int ipv;
@ -291,6 +398,7 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr
} }
tinfo = TUNNEL_INFO(.interface = interface, tinfo = TUNNEL_INFO(.interface = interface,
.protocol = protocol, .protocol = protocol,
.host_name = host_name,
.name = name, .name = name,
.type = type, .type = type,
.domain = domain); .domain = domain);
@ -302,21 +410,14 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr
} }
avahi_address_snprint(at, sizeof(at), a); avahi_address_snprint(at, sizeof(at), a);
ipv = protocol == AVAHI_PROTO_INET ? 4 : 6;
pw_properties_setf(props, "raop.ip", "%s", at); pw_properties_setf(props, "raop.ip", "%s", at);
pw_properties_setf(props, "raop.ip.version", "%d", ipv);
pw_properties_setf(props, "raop.port", "%u", port); pw_properties_setf(props, "raop.port", "%u", port);
pw_properties_setf(props, "raop.name", "%s", name);
pw_properties_setf(props, "raop.hostname", "%s", host_name); pw_properties_setf(props, "raop.hostname", "%s", host_name);
pw_properties_setf(props, "raop.domain", "%s", domain);
ipv = protocol == AVAHI_PROTO_INET ? 4 : 6;
if ((str = strstr(name, "@"))) {
str++;
if (strlen(str) > 0)
name = str;
}
pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
"RAOP on %s (IPv%d)", name, ipv);
pw_properties_setf(props, PW_KEY_NODE_NAME, "raop_sink.%s.%s.ipv%d",
name, host_name, ipv);
for (l = txt; l; l = l->next) { for (l = txt; l; l = l->next) {
char *key, *value; char *key, *value;
@ -329,45 +430,24 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr
avahi_free(value); avahi_free(value);
} }
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,
.tinfo = &tinfo,
};
pw_conf_match_rules(str, strlen(str), NAME, &props->dict,
rule_matched, &minfo);
if ((f = open_memstream(&args, &size)) == NULL) { if (!minfo.matched)
pw_log_error("Can't open memstream: %m"); pw_log_info("unmatched service found %s", str);
goto done;
} }
fprintf(f, "{");
pw_properties_serialize_dict(f, &props->dict, 0);
fprintf(f, " stream.props = {");
fprintf(f, " }");
fprintf(f, "}");
fclose(f);
pw_properties_free(props);
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) {
pw_log_error("Can't load module: %m");
goto done;
}
t = make_tunnel(impl, &tinfo);
if (t == NULL) {
pw_log_error("Can't make tunnel: %m");
pw_impl_module_destroy(mod);
goto done;
}
pw_impl_module_add_listener(mod, &t->module_listener, &submodule_events, t);
t->module = mod;
done: done:
avahi_service_resolver_free(r); avahi_service_resolver_free(r);
pw_properties_free(props);
} }

View file

@ -57,8 +57,9 @@
* Options specific to the behavior of this module * Options specific to the behavior of this module
* *
* - `raop.ip`: The ip address of the remote end. * - `raop.ip`: The ip address of the remote end.
* - `raop.hostname`: The hostname of the remote end.
* - `raop.port`: The port of the remote end. * - `raop.port`: The port of the remote end.
* - `raop.name`: The name of the remote end.
* - `raop.hostname`: The hostname of the remote end.
* - `raop.transport`: The data transport to use, one of "udp" or "tcp". Defaults * - `raop.transport`: The data transport to use, one of "udp" or "tcp". Defaults
* to "udp". * to "udp".
* - `raop.encryption.type`: The encryption type to use. One of "none", "RSA" or * - `raop.encryption.type`: The encryption type to use. One of "none", "RSA" or
@ -89,8 +90,9 @@
* args = { * args = {
* # Set the remote address to tunnel to * # Set the remote address to tunnel to
* raop.ip = "127.0.0.1" * raop.ip = "127.0.0.1"
* raop.hostname = "my-raop-device"
* raop.port = 8190 * raop.port = 8190
* raop.name = "my-raop-device"
* raop.hostname = "My Service"
* #raop.transport = "udp" * #raop.transport = "udp"
* raop.encryption.type = "RSA" * raop.encryption.type = "RSA"
* #raop.audio.codec = "PCM" * #raop.audio.codec = "PCM"
@ -144,8 +146,9 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
#define DEFAULT_LATENCY 22050 #define DEFAULT_LATENCY 22050
#define MODULE_USAGE "[ raop.ip=<ip address of host> ] " \ #define MODULE_USAGE "[ raop.ip=<ip address of host> ] " \
"[ raop.hostname=<name of host> ] " \
"[ raop.port=<remote port> ] " \ "[ raop.port=<remote port> ] " \
"[ raop.name=<name of host> ] " \
"[ raop.hostname=<hostname of host> ] " \
"[ raop.transport=<transport, default:udp> ] " \ "[ raop.transport=<transport, default:udp> ] " \
"[ raop.encryption.type=<encryption, default:none> ] " \ "[ raop.encryption.type=<encryption, default:none> ] " \
"[ raop.audio.codec=PCM ] " \ "[ raop.audio.codec=PCM ] " \
@ -1522,8 +1525,7 @@ static int rtsp_do_teardown(struct impl *impl)
if (!impl->ready) if (!impl->ready)
return 0; return 0;
return pw_rtsp_client_send(impl->rtsp, "TEARDOWN", NULL, return rtsp_send(impl, "TEARDOWN", NULL, NULL, rtsp_teardown_reply);
NULL, NULL, rtsp_teardown_reply, impl);
} }
static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param) static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
@ -1759,7 +1761,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
struct pw_context *context = pw_impl_module_get_context(module); struct pw_context *context = pw_impl_module_get_context(module);
struct pw_properties *props = NULL; struct pw_properties *props = NULL;
struct impl *impl; struct impl *impl;
const char *str; const char *str, *name, *hostname, *ipv;
int res; int res;
PW_LOG_TOPIC_INIT(mod_topic); PW_LOG_TOPIC_INIT(mod_topic);
@ -1806,13 +1808,25 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) if ((name = pw_properties_get(props, "raop.name")) == NULL)
pw_properties_setf(props, PW_KEY_NODE_NAME, "raop_output.%s", name = "RAOP";
pw_properties_get(props, "raop.hostname"));
if ((str = strstr(name, "@"))) {
str++;
if (strlen(str) > 0)
name = str;
}
if ((ipv = pw_properties_get(props, "raop.ip.version")) == NULL)
ipv = "4";
if ((hostname = pw_properties_get(props, "raop.hostname")) == NULL)
hostname = name;
if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
pw_properties_get(props, PW_KEY_NODE_NAME)); "%s (IPv%s)", name, ipv);
if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
pw_properties_setf(props, PW_KEY_NODE_NAME, "raop_sink.%s.ipv%s",
hostname, ipv);
if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL) if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
pw_properties_set(props, PW_KEY_NODE_LATENCY, "352/44100"); pw_properties_set(props, PW_KEY_NODE_LATENCY, "352/44100");