mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-11-03 09:01:54 -05:00
module-raop: add match rules for discover
Useful for selecting only ip4 streams or for setting up the password.
This commit is contained in:
parent
9e56fae236
commit
8167e1b9be
2 changed files with 160 additions and 66 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue