pulse-server: port zeroconf publish to helper

This commit is contained in:
Wim Taymans 2026-02-27 12:22:52 +01:00
parent c09bcfdc97
commit a1db2b8d35

View file

@ -15,14 +15,7 @@
#include "../module.h"
#include "../pulse-server.h"
#include "../server.h"
#include "../../zeroconf-utils/avahi-poll.h"
#include <avahi-client/client.h>
#include <avahi-client/publish.h>
#include <avahi-common/alternative.h>
#include <avahi-common/error.h>
#include <avahi-common/domain.h>
#include <avahi-common/malloc.h>
#include "../../zeroconf-utils/zeroconf.h"
/** \page page_pulse_module_zeroconf_publish Zeroconf Publish
*
@ -52,32 +45,17 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
#define SERVICE_DATA_ID "module-zeroconf-publish.service"
enum service_subtype {
SUBTYPE_HARDWARE,
SUBTYPE_VIRTUAL,
SUBTYPE_MONITOR
};
struct service {
struct spa_list link;
struct module_zeroconf_publish_data *userdata;
AvahiEntryGroup *entry_group;
AvahiStringList *txt;
struct server *server;
const char *service_type;
enum service_subtype subtype;
char *name;
bool is_sink;
struct sample_spec ss;
struct channel_map cm;
struct pw_properties *props;
char service_name[AVAHI_LABEL_MAX];
unsigned published:1;
};
@ -91,8 +69,8 @@ struct module_zeroconf_publish_data {
struct spa_hook manager_listener;
struct spa_hook impl_listener;
AvahiPoll *avahi_poll;
AvahiClient *client;
struct pw_zeroconf *zeroconf;
struct spa_hook zeroconf_listener;
/* lists of services */
struct spa_list pending;
@ -116,47 +94,43 @@ static const struct pw_core_events core_events = {
.error = on_core_error,
};
static void get_service_name(struct pw_manager_object *o, char *buf, size_t length)
static void unpublish_service(struct service *s)
{
const char *hn, *un, *n;
const char *device;
hn = pw_get_host_name();
un = pw_get_user_name();
n = pw_properties_get(o->props, PW_KEY_NODE_DESCRIPTION);
spa_list_remove(&s->link);
spa_list_append(&s->userdata->pending, &s->link);
s->published = false;
s->server = NULL;
snprintf(buf, length, "%s@%s: %s", un, hn, n);
device = pw_properties_get(s->props, "device");
pw_log_info("unpublished service: %s", device);
pw_zeroconf_set_announce(s->userdata->zeroconf, s, NULL);
}
static void unpublish_all_services(struct module_zeroconf_publish_data *d)
{
struct service *s;
spa_list_consume(s, &d->published, link)
unpublish_service(s);
}
static void service_free(struct service *s)
{
pw_log_debug("service %p: free", s);
if (s->entry_group)
avahi_entry_group_free(s->entry_group);
if (s->name)
free(s->name);
if (s->published)
unpublish_service(s);
pw_properties_free(s->props);
avahi_string_list_free(s->txt);
spa_list_remove(&s->link);
/* no need to free, the service is added as custom
* data on the object */
}
static void unpublish_service(struct service *s)
{
spa_list_remove(&s->link);
spa_list_append(&s->userdata->pending, &s->link);
s->published = false;
s->server = NULL;
}
static void unpublish_all_services(struct module_zeroconf_publish_data *d)
{
struct service *s;
spa_list_consume(s, &d->published, link)
unpublish_service(s);
}
#define PA_CHANNEL_MAP_SNPRINT_MAX (CHANNELS_MAX * 32)
static char* channel_map_snprint(char *s, size_t l, const struct channel_map *map)
{
@ -188,6 +162,39 @@ static char* channel_map_snprint(char *s, size_t l, const struct channel_map *ma
return s;
}
static void txt_record_server_data(struct pw_core_info *info, struct pw_properties *props)
{
struct utsname u;
spa_assert(info);
pw_properties_set(props, "server-version", PACKAGE_NAME" "PACKAGE_VERSION);
pw_properties_set(props, "user-name", pw_get_user_name());
pw_properties_set(props, "fqdn", pw_get_host_name());
pw_properties_setf(props, "cookie", "0x%08x", info->cookie);
if (uname(&u) >= 0)
pw_properties_setf(props, "uname", "%s %s %s", u.sysname, u.machine, u.release);
}
static void fill_service_txt(const struct service *s, const struct pw_properties *props)
{
static const struct mapping {
const char *pw_key, *txt_key;
} mappings[] = {
{ PW_KEY_NODE_DESCRIPTION, "description" },
{ PW_KEY_DEVICE_VENDOR_NAME, "vendor-name" },
{ PW_KEY_DEVICE_PRODUCT_NAME, "product-name" },
{ PW_KEY_DEVICE_CLASS, "class" },
{ PW_KEY_DEVICE_FORM_FACTOR, "form-factor" },
{ PW_KEY_DEVICE_ICON_NAME, "icon-name" },
};
SPA_FOR_EACH_ELEMENT_VAR(mappings, m) {
const char *value = pw_properties_get(props, m->pw_key);
if (value != NULL)
pw_properties_set(s->props, m->txt_key, value);
}
}
static void fill_service_data(struct module_zeroconf_publish_data *d, struct service *s,
struct pw_manager_object *o)
{
@ -200,6 +207,10 @@ static void fill_service_data(struct module_zeroconf_publish_data *d, struct ser
struct card_info card_info = CARD_INFO_INIT;
struct device_info dev_info;
uint32_t flags = 0;
const char *service_type, *subtype, *subtype_service[2];
uint32_t n_subtype = 0;
char cm[PA_CHANNEL_MAP_SNPRINT_MAX];
if (info == NULL || info->props == NULL)
return;
@ -228,19 +239,49 @@ static void fill_service_data(struct module_zeroconf_publish_data *d, struct ser
s->ss = dev_info.ss;
s->cm = dev_info.map;
s->name = strdup(name);
s->props = pw_properties_copy(o->props);
s->props = pw_properties_new(NULL, NULL);
txt_record_server_data(s->userdata->manager->info, s->props);
if (is_sink) {
s->is_sink = true;
s->service_type = SERVICE_TYPE_SINK;
s->subtype = flags & SINK_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
service_type = SERVICE_TYPE_SINK;
if (flags & SINK_HARDWARE) {
subtype = "hardware";
subtype_service[n_subtype++] = SERVICE_SUBTYPE_SINK_HARDWARE;
} else {
subtype = "virtual";
subtype_service[n_subtype++] = SERVICE_SUBTYPE_SINK_VIRTUAL;
}
} else if (is_source) {
s->is_sink = false;
s->service_type = SERVICE_TYPE_SOURCE;
s->subtype = flags & SOURCE_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
service_type = SERVICE_TYPE_SOURCE;
if (flags & SOURCE_HARDWARE) {
subtype = "hardware";
subtype_service[n_subtype++] = SERVICE_SUBTYPE_SOURCE_HARDWARE;
} else {
subtype = "virtual";
subtype_service[n_subtype++] = SERVICE_SUBTYPE_SOURCE_VIRTUAL;
}
subtype_service[n_subtype++] = SERVICE_SUBTYPE_SOURCE_NON_MONITOR;
} else
spa_assert_not_reached();
pw_properties_set(s->props, "device", name);
pw_properties_setf(s->props, "rate", "%u", s->ss.rate);
pw_properties_setf(s->props, "channels", "%u", s->ss.channels);
pw_properties_set(s->props, "format", format_id2paname(s->ss.format));
pw_properties_set(s->props, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm));
pw_properties_set(s->props, "subtype", subtype);
pw_properties_setf(s->props, "zeroconf.session", "%s@%s: %s",
pw_get_user_name(), pw_get_host_name(), desc);
pw_properties_set(s->props, "zeroconf.service", service_type);
pw_properties_setf(s->props, "zeroconf.subtypes", "[ %s%s%s ]",
n_subtype > 0 ? subtype_service[0] : "",
n_subtype > 1 ? ", " : "",
n_subtype > 1 ? subtype_service[1] : "");
fill_service_txt(s, o->props);
}
static struct service *create_service(struct module_zeroconf_publish_data *d, struct pw_manager_object *o)
@ -252,8 +293,6 @@ static struct service *create_service(struct module_zeroconf_publish_data *d, st
return NULL;
s->userdata = d;
s->entry_group = NULL;
get_service_name(o, s->service_name, sizeof(s->service_name));
spa_list_append(&d->pending, &s->link);
fill_service_data(d, s, o);
@ -263,127 +302,6 @@ static struct service *create_service(struct module_zeroconf_publish_data *d, st
return s;
}
static AvahiStringList* txt_record_server_data(struct pw_core_info *info, AvahiStringList *l)
{
const char *t;
struct utsname u;
spa_assert(info);
l = avahi_string_list_add_pair(l, "server-version", PACKAGE_NAME" "PACKAGE_VERSION);
t = pw_get_user_name();
l = avahi_string_list_add_pair(l, "user-name", t);
if (uname(&u) >= 0) {
char sysname[sizeof(u.sysname) + sizeof(u.machine) + sizeof(u.release)];
snprintf(sysname, sizeof(sysname), "%s %s %s", u.sysname, u.machine, u.release);
l = avahi_string_list_add_pair(l, "uname", sysname);
}
t = pw_get_host_name();
l = avahi_string_list_add_pair(l, "fqdn", t);
l = avahi_string_list_add_printf(l, "cookie=0x%08x", info->cookie);
return l;
}
static void clear_entry_group(struct service *s)
{
if (s->entry_group == NULL)
return;
avahi_entry_group_free(s->entry_group);
s->entry_group = NULL;
}
static void publish_service(struct service *s);
static void service_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
{
struct service *s = userdata;
spa_assert(s);
if (!s->published) {
pw_log_info("cancel unpublished service: %s", s->service_name);
clear_entry_group(s);
return;
}
switch (state) {
case AVAHI_ENTRY_GROUP_ESTABLISHED:
pw_log_info("established service: %s", s->service_name);
break;
case AVAHI_ENTRY_GROUP_COLLISION:
{
char *t;
t = avahi_alternative_service_name(s->service_name);
pw_log_info("service name collision: renaming '%s' to '%s'", s->service_name, t);
snprintf(s->service_name, sizeof(s->service_name), "%s", t);
avahi_free(t);
unpublish_service(s);
publish_service(s);
break;
}
case AVAHI_ENTRY_GROUP_FAILURE:
pw_log_error("failed to establish service '%s': %s",
s->service_name,
avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
unpublish_service(s);
clear_entry_group(s);
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
break;
}
}
#define PA_CHANNEL_MAP_SNPRINT_MAX (CHANNELS_MAX * 32)
static AvahiStringList *get_service_txt(const struct service *s)
{
static const char * const subtype_text[] = {
[SUBTYPE_HARDWARE] = "hardware",
[SUBTYPE_VIRTUAL] = "virtual",
[SUBTYPE_MONITOR] = "monitor"
};
static const struct mapping {
const char *pw_key, *txt_key;
} mappings[] = {
{ PW_KEY_NODE_DESCRIPTION, "description" },
{ PW_KEY_DEVICE_VENDOR_NAME, "vendor-name" },
{ PW_KEY_DEVICE_PRODUCT_NAME, "product-name" },
{ PW_KEY_DEVICE_CLASS, "class" },
{ PW_KEY_DEVICE_FORM_FACTOR, "form-factor" },
{ PW_KEY_DEVICE_ICON_NAME, "icon-name" },
};
char cm[PA_CHANNEL_MAP_SNPRINT_MAX];
AvahiStringList *txt = NULL;
txt = txt_record_server_data(s->userdata->manager->info, txt);
txt = avahi_string_list_add_pair(txt, "device", s->name);
txt = avahi_string_list_add_printf(txt, "rate=%u", s->ss.rate);
txt = avahi_string_list_add_printf(txt, "channels=%u", s->ss.channels);
txt = avahi_string_list_add_pair(txt, "format", format_id2paname(s->ss.format));
txt = avahi_string_list_add_pair(txt, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm));
txt = avahi_string_list_add_pair(txt, "subtype", subtype_text[s->subtype]);
SPA_FOR_EACH_ELEMENT_VAR(mappings, m) {
const char *value = pw_properties_get(s->props, m->pw_key);
if (value != NULL)
txt = avahi_string_list_add_pair(txt, m->txt_key, value);
}
return txt;
}
static struct server *find_server(struct service *s, int *proto, uint16_t *port)
{
struct module_zeroconf_publish_data *d = s->userdata;
@ -392,109 +310,47 @@ static struct server *find_server(struct service *s, int *proto, uint16_t *port)
spa_list_for_each(server, &impl->servers, link) {
if (server->addr.ss_family == AF_INET) {
*proto = AVAHI_PROTO_INET;
*proto = 4;
*port = ntohs(((struct sockaddr_in*) &server->addr)->sin_port);
return server;
} else if (server->addr.ss_family == AF_INET6) {
*proto = AVAHI_PROTO_INET6;
*proto = 6;
*port = ntohs(((struct sockaddr_in6*) &server->addr)->sin6_port);
return server;
}
}
return NULL;
}
static void publish_service(struct service *s)
{
struct module_zeroconf_publish_data *d = s->userdata;
int proto;
int proto, res;
uint16_t port;
struct server *server = find_server(s, &proto, &port);
const char *device;
if (!server)
return;
device = pw_properties_get(s->props, "device");
pw_log_debug("found server:%p proto:%d port:%d", server, proto, port);
if (!d->client || avahi_client_get_state(d->client) != AVAHI_CLIENT_S_RUNNING)
pw_properties_setf(s->props, "zeroconf.proto", "%d", proto);
pw_properties_setf(s->props, "zeroconf.port", "%d", port);
if ((res = pw_zeroconf_set_announce(s->userdata->zeroconf, s, &s->props->dict)) < 0) {
pw_log_error("failed to announce service %s: %s", device, spa_strerror(res));
return;
s->published = true;
if (!s->entry_group) {
s->entry_group = avahi_entry_group_new(d->client, service_entry_group_callback, s);
if (s->entry_group == NULL) {
pw_log_error("avahi_entry_group_new(): %s",
avahi_strerror(avahi_client_errno(d->client)));
goto error;
}
} else {
avahi_entry_group_reset(s->entry_group);
}
if (s->txt == NULL)
s->txt = get_service_txt(s);
if (avahi_entry_group_add_service_strlst(
s->entry_group,
AVAHI_IF_UNSPEC, proto,
0,
s->service_name,
s->service_type,
NULL,
NULL,
port,
s->txt) < 0) {
pw_log_error("avahi_entry_group_add_service_strlst(): %s",
avahi_strerror(avahi_client_errno(d->client)));
goto error;
}
if (avahi_entry_group_add_service_subtype(
s->entry_group,
AVAHI_IF_UNSPEC, proto,
0,
s->service_name,
s->service_type,
NULL,
s->is_sink ? (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SINK_HARDWARE : SERVICE_SUBTYPE_SINK_VIRTUAL) :
(s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SOURCE_HARDWARE : (s->subtype == SUBTYPE_VIRTUAL ? SERVICE_SUBTYPE_SOURCE_VIRTUAL : SERVICE_SUBTYPE_SOURCE_MONITOR))) < 0) {
pw_log_error("avahi_entry_group_add_service_subtype(): %s",
avahi_strerror(avahi_client_errno(d->client)));
goto error;
}
if (!s->is_sink && s->subtype != SUBTYPE_MONITOR) {
if (avahi_entry_group_add_service_subtype(
s->entry_group,
AVAHI_IF_UNSPEC, proto,
0,
s->service_name,
SERVICE_TYPE_SOURCE,
NULL,
SERVICE_SUBTYPE_SOURCE_NON_MONITOR) < 0) {
pw_log_error("avahi_entry_group_add_service_subtype(): %s",
avahi_strerror(avahi_client_errno(d->client)));
goto error;
}
}
if (avahi_entry_group_commit(s->entry_group) < 0) {
pw_log_error("avahi_entry_group_commit(): %s",
avahi_strerror(avahi_client_errno(d->client)));
goto error;
}
spa_list_remove(&s->link);
spa_list_append(&d->published, &s->link);
s->published = true;
s->server = server;
pw_log_info("created service: %s", s->service_name);
return;
error:
s->published = false;
pw_log_info("published service: %s", device);
return;
}
@ -506,62 +362,6 @@ static void publish_pending(struct module_zeroconf_publish_data *data)
publish_service(s);
}
static void clear_pending_entry_groups(struct module_zeroconf_publish_data *data)
{
struct service *s;
spa_list_for_each(s, &data->pending, link)
clear_entry_group(s);
}
static void client_callback(AvahiClient *c, AvahiClientState state, void *d)
{
struct module_zeroconf_publish_data *data = d;
spa_assert(c);
spa_assert(data);
data->client = c;
switch (state) {
case AVAHI_CLIENT_S_RUNNING:
pw_log_info("the avahi daemon is up and running");
publish_pending(data);
break;
case AVAHI_CLIENT_S_COLLISION:
pw_log_error("host name collision");
unpublish_all_services(d);
break;
case AVAHI_CLIENT_FAILURE:
{
int err = avahi_client_errno(data->client);
pw_log_error("avahi client failure: %s", avahi_strerror(err));
unpublish_all_services(data);
clear_pending_entry_groups(data);
avahi_client_free(data->client);
data->client = NULL;
if (err == AVAHI_ERR_DISCONNECTED) {
data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, data, &err);
if (data->client == NULL)
pw_log_error("failed to create avahi client: %s", avahi_strerror(err));
}
if (data->client == NULL)
module_schedule_unload(data->module);
break;
}
case AVAHI_CLIENT_CONNECTING:
pw_log_info("connecting to the avahi daemon...");
break;
default:
break;
}
}
static void manager_removed(void *d, struct pw_manager_object *o)
{
if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source(o))
@ -624,7 +424,6 @@ static void impl_server_stopped(void *data, struct server *server)
if (s->server == server)
unpublish_service(s);
}
publish_pending(d);
}
@ -634,10 +433,18 @@ static const struct impl_events impl_events = {
.server_stopped = impl_server_stopped,
};
static void on_zeroconf_error(void *data, int err, const char *message)
{
pw_log_error("got zeroconf error %d: %s", err, message);
}
static const struct pw_zeroconf_events zeroconf_events = {
PW_VERSION_ZEROCONF_EVENTS,
.error = on_zeroconf_error,
};
static int module_zeroconf_publish_load(struct module *module)
{
struct module_zeroconf_publish_data *data = module->user_data;
int error;
data->core = pw_context_connect(module->impl->context, NULL, 0);
if (data->core == NULL) {
@ -649,24 +456,22 @@ static int module_zeroconf_publish_load(struct module *module)
&data->core_listener,
&core_events, data);
data->avahi_poll = pw_avahi_poll_new(module->impl->context);
data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL,
client_callback, data, &error);
if (!data->client) {
pw_log_error("failed to create avahi client: %s", avahi_strerror(error));
return -errno;
}
data->manager = pw_manager_new(data->core);
if (data->manager == NULL) {
pw_log_error("failed to create pipewire manager: %m");
return -errno;
}
pw_manager_add_listener(data->manager, &data->manager_listener,
&manager_events, data);
data->zeroconf = pw_zeroconf_new(module->impl->context, NULL);
if (!data->zeroconf) {
pw_log_error("failed to create zeroconf: %m");
return -errno;
}
pw_zeroconf_add_listener(data->zeroconf, &data->zeroconf_listener,
&zeroconf_events, data);
impl_add_listener(module->impl, &data->impl_listener, &impl_events, data);
return 0;
@ -684,22 +489,18 @@ static int module_zeroconf_publish_unload(struct module *module)
spa_list_consume(s, &d->pending, link)
service_free(s);
if (d->client)
avahi_client_free(d->client);
if (d->avahi_poll)
pw_avahi_poll_free(d->avahi_poll);
if (d->zeroconf) {
spa_hook_remove(&d->zeroconf_listener);
pw_zeroconf_destroy(d->zeroconf);
}
if (d->manager != NULL) {
spa_hook_remove(&d->manager_listener);
pw_manager_destroy(d->manager);
}
if (d->core != NULL) {
spa_hook_remove(&d->core_listener);
pw_core_disconnect(d->core);
}
return 0;
}