mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-03-13 05:33:55 -04:00
modules: move zeroconf code to zeroconf-utils
This commit is contained in:
parent
4a399172b6
commit
fa04146cfb
12 changed files with 15 additions and 16 deletions
|
|
@ -1,558 +0,0 @@
|
|||
/* PipeWire */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/ip.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <spa/utils/result.h>
|
||||
#include <spa/utils/json.h>
|
||||
|
||||
#include <avahi-client/lookup.h>
|
||||
#include <avahi-client/publish.h>
|
||||
#include <avahi-common/error.h>
|
||||
#include <avahi-common/malloc.h>
|
||||
|
||||
#include "../module-zeroconf-discover/avahi-poll.h"
|
||||
|
||||
#include "zeroconf.h"
|
||||
|
||||
#define pw_zeroconf_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_zeroconf_events, m, v, ##__VA_ARGS__)
|
||||
#define pw_zeroconf_emit_destroy(c) pw_zeroconf_emit(c, destroy, 0)
|
||||
#define pw_zeroconf_emit_error(c,e,m) pw_zeroconf_emit(c, error, 0, e, m)
|
||||
#define pw_zeroconf_emit_added(c,id,i) pw_zeroconf_emit(c, added, 0, id, i)
|
||||
#define pw_zeroconf_emit_removed(c,id,i) pw_zeroconf_emit(c, removed, 0, id, i)
|
||||
|
||||
struct service_info {
|
||||
AvahiIfIndex interface;
|
||||
AvahiProtocol protocol;
|
||||
const char *name;
|
||||
const char *type;
|
||||
const char *domain;
|
||||
const char *host_name;
|
||||
AvahiAddress address;
|
||||
uint16_t port;
|
||||
};
|
||||
|
||||
#define SERVICE_INFO(...) ((struct service_info){ __VA_ARGS__ })
|
||||
|
||||
struct entry {
|
||||
struct pw_zeroconf *zc;
|
||||
struct spa_list link;
|
||||
|
||||
#define TYPE_ANNOUNCE 0
|
||||
#define TYPE_BROWSE 1
|
||||
uint32_t type;
|
||||
void *user;
|
||||
|
||||
struct pw_properties *props;
|
||||
|
||||
AvahiEntryGroup *group;
|
||||
AvahiServiceBrowser *browser;
|
||||
|
||||
struct spa_list services;
|
||||
};
|
||||
|
||||
struct service {
|
||||
struct entry *e;
|
||||
struct spa_list link;
|
||||
|
||||
struct service_info info;
|
||||
|
||||
struct pw_properties *props;
|
||||
};
|
||||
|
||||
struct pw_zeroconf {
|
||||
int refcount;
|
||||
struct pw_context *context;
|
||||
|
||||
struct pw_properties *props;
|
||||
|
||||
struct spa_hook_list listener_list;
|
||||
|
||||
AvahiPoll *poll;
|
||||
AvahiClient *client;
|
||||
AvahiClientState state;
|
||||
|
||||
struct spa_list entries;
|
||||
|
||||
bool discover_local;
|
||||
};
|
||||
|
||||
static struct service *service_find(struct entry *e, const struct service_info *info)
|
||||
{
|
||||
struct service *s;
|
||||
spa_list_for_each(s, &e->services, link) {
|
||||
if (s->info.interface == info->interface &&
|
||||
s->info.protocol == info->protocol &&
|
||||
spa_streq(s->info.name, info->name) &&
|
||||
spa_streq(s->info.type, info->type) &&
|
||||
spa_streq(s->info.domain, info->domain))
|
||||
return s;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void service_free(struct service *s)
|
||||
{
|
||||
spa_list_remove(&s->link);
|
||||
free((void*)s->info.name);
|
||||
free((void*)s->info.type);
|
||||
free((void*)s->info.domain);
|
||||
free((void*)s->info.host_name);
|
||||
pw_properties_free(s->props);
|
||||
free(s);
|
||||
}
|
||||
|
||||
struct entry *entry_find(struct pw_zeroconf *zc, uint32_t type, void *user)
|
||||
{
|
||||
struct entry *e;
|
||||
spa_list_for_each(e, &zc->entries, link)
|
||||
if (e->type == type && e->user == user)
|
||||
return e;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void entry_free(struct entry *e)
|
||||
{
|
||||
struct service *s;
|
||||
|
||||
spa_list_remove(&e->link);
|
||||
if (e->group)
|
||||
avahi_entry_group_free(e->group);
|
||||
spa_list_consume(s, &e->services, link)
|
||||
service_free(s);
|
||||
pw_properties_free(e->props);
|
||||
free(e);
|
||||
}
|
||||
|
||||
static void zeroconf_free(struct pw_zeroconf *zc)
|
||||
{
|
||||
struct entry *a;
|
||||
|
||||
spa_list_consume(a, &zc->entries, link)
|
||||
entry_free(a);
|
||||
|
||||
if (zc->client)
|
||||
avahi_client_free(zc->client);
|
||||
if (zc->poll)
|
||||
pw_avahi_poll_free(zc->poll);
|
||||
pw_properties_free(zc->props);
|
||||
free(zc);
|
||||
}
|
||||
|
||||
static void zeroconf_unref(struct pw_zeroconf *zc)
|
||||
{
|
||||
if (--zc->refcount == 0)
|
||||
zeroconf_free(zc);
|
||||
}
|
||||
|
||||
void pw_zeroconf_destroy(struct pw_zeroconf *zc)
|
||||
{
|
||||
pw_zeroconf_emit_destroy(zc);
|
||||
|
||||
zeroconf_unref(zc);
|
||||
}
|
||||
|
||||
static struct service *service_new(struct entry *e,
|
||||
const struct service_info *info, AvahiStringList *txt)
|
||||
{
|
||||
struct service *s;
|
||||
struct pw_zeroconf *zc = e->zc;
|
||||
const AvahiAddress *a = &info->address;
|
||||
static const char *link_local_range = "169.254.";
|
||||
AvahiStringList *l;
|
||||
char at[AVAHI_ADDRESS_STR_MAX], if_suffix[16] = "";
|
||||
|
||||
if ((s = calloc(1, sizeof(*s))) == NULL)
|
||||
goto error;
|
||||
|
||||
s->e = e;
|
||||
spa_list_append(&e->services, &s->link);
|
||||
|
||||
s->info.interface = info->interface;
|
||||
s->info.protocol = info->protocol;
|
||||
s->info.name = strdup(info->name);
|
||||
s->info.type = strdup(info->type);
|
||||
s->info.domain = strdup(info->domain);
|
||||
s->info.host_name = strdup(info->host_name);
|
||||
s->info.address = info->address;
|
||||
s->info.port = info->port;
|
||||
|
||||
if ((s->props = pw_properties_new(NULL, NULL)) == NULL)
|
||||
goto error;
|
||||
|
||||
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", info->interface);
|
||||
|
||||
avahi_address_snprint(at, sizeof(at), a);
|
||||
if (a->proto == AVAHI_PROTO_INET &&
|
||||
spa_strstartswith(at, link_local_range))
|
||||
snprintf(if_suffix, sizeof(if_suffix), "%%%d", info->interface);
|
||||
|
||||
pw_properties_setf(s->props, "zeroconf.ifindex", "%d", info->interface);
|
||||
pw_properties_setf(s->props, "zeroconf.name", "%s", info->name);
|
||||
pw_properties_setf(s->props, "zeroconf.type", "%s", info->type);
|
||||
pw_properties_setf(s->props, "zeroconf.domain", "%s", info->domain);
|
||||
pw_properties_setf(s->props, "zeroconf.hostname", "%s", info->host_name);
|
||||
pw_properties_setf(s->props, "zeroconf.address", "%s%s", at, if_suffix);
|
||||
pw_properties_setf(s->props, "zeroconf.port", "%u", info->port);
|
||||
|
||||
for (l = txt; l; l = l->next) {
|
||||
char *key, *value;
|
||||
|
||||
if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
|
||||
break;
|
||||
|
||||
pw_properties_set(s->props, key, value);
|
||||
avahi_free(key);
|
||||
avahi_free(value);
|
||||
}
|
||||
|
||||
pw_log_info("new %s %s %s %s", info->name, info->type, info->domain, info->host_name);
|
||||
pw_zeroconf_emit_added(zc, e->user, &s->props->dict);
|
||||
|
||||
return s;
|
||||
|
||||
error:
|
||||
if (s)
|
||||
service_free(s);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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 entry *e = userdata;
|
||||
struct pw_zeroconf *zc = e->zc;
|
||||
struct service_info info;
|
||||
|
||||
if (event != AVAHI_RESOLVER_FOUND) {
|
||||
pw_log_error("Resolving of '%s' failed: %s", name,
|
||||
avahi_strerror(avahi_client_errno(zc->client)));
|
||||
goto done;
|
||||
}
|
||||
|
||||
info = SERVICE_INFO(.interface = interface,
|
||||
.protocol = protocol,
|
||||
.name = name,
|
||||
.type = type,
|
||||
.domain = domain,
|
||||
.host_name = host_name,
|
||||
.address = *a,
|
||||
.port = port);
|
||||
|
||||
service_new(e, &info, txt);
|
||||
done:
|
||||
avahi_service_resolver_free(r);
|
||||
}
|
||||
|
||||
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 entry *e = userdata;
|
||||
struct pw_zeroconf *zc = e->zc;
|
||||
struct service_info info;
|
||||
struct service *s;
|
||||
|
||||
if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !zc->discover_local)
|
||||
return;
|
||||
|
||||
info = SERVICE_INFO(.interface = interface,
|
||||
.protocol = protocol,
|
||||
.name = name,
|
||||
.type = type,
|
||||
.domain = domain);
|
||||
|
||||
s = service_find(e, &info);
|
||||
|
||||
switch (event) {
|
||||
case AVAHI_BROWSER_NEW:
|
||||
if (s != NULL)
|
||||
return;
|
||||
if (!(avahi_service_resolver_new(zc->client,
|
||||
interface, protocol,
|
||||
name, type, domain,
|
||||
AVAHI_PROTO_UNSPEC, 0,
|
||||
resolver_cb, e))) {
|
||||
int res = avahi_client_errno(zc->client);
|
||||
pw_log_error("can't make service resolver: %s", avahi_strerror(res));
|
||||
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
|
||||
}
|
||||
break;
|
||||
case AVAHI_BROWSER_REMOVE:
|
||||
if (s == NULL)
|
||||
return;
|
||||
pw_log_info("removed %s %s %s", name, type, domain);
|
||||
pw_zeroconf_emit_removed(zc, e->user, &s->props->dict);
|
||||
service_free(s);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_browse(struct pw_zeroconf *zc, struct entry *e)
|
||||
{
|
||||
const struct spa_dict_item *it;
|
||||
const char *service_name = NULL;
|
||||
int res;
|
||||
|
||||
if (e->browser == NULL) {
|
||||
spa_dict_for_each(it, &e->props->dict) {
|
||||
if (spa_streq(it->key, "zeroconf.service"))
|
||||
service_name = it->value;
|
||||
}
|
||||
if (service_name == NULL) {
|
||||
res = -EINVAL;
|
||||
pw_log_error("no service provided");
|
||||
pw_zeroconf_emit_error(zc, res, spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
e->browser = avahi_service_browser_new(zc->client,
|
||||
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
||||
service_name, NULL, 0,
|
||||
browser_cb, e);
|
||||
if (e->browser == NULL) {
|
||||
res = avahi_client_errno(zc->client);
|
||||
pw_log_error("can't make browser: %s", avahi_strerror(res));
|
||||
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
|
||||
return -EIO;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
|
||||
{
|
||||
struct entry *e = userdata;
|
||||
struct pw_zeroconf *zc = e->zc;
|
||||
int res;
|
||||
|
||||
zc->refcount++;
|
||||
|
||||
switch (state) {
|
||||
case AVAHI_ENTRY_GROUP_ESTABLISHED:
|
||||
pw_log_info("Service successfully established");
|
||||
break;
|
||||
case AVAHI_ENTRY_GROUP_COLLISION:
|
||||
pw_log_error("Service name collision");
|
||||
break;
|
||||
case AVAHI_ENTRY_GROUP_FAILURE:
|
||||
res = avahi_client_errno(zc->client);
|
||||
pw_log_error("Entry group failure: %s", avahi_strerror(res));
|
||||
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
|
||||
break;
|
||||
case AVAHI_ENTRY_GROUP_UNCOMMITED:
|
||||
case AVAHI_ENTRY_GROUP_REGISTERING:
|
||||
break;
|
||||
}
|
||||
zeroconf_unref(zc);
|
||||
}
|
||||
|
||||
static int do_announce(struct pw_zeroconf *zc, struct entry *e)
|
||||
{
|
||||
AvahiStringList *txt = NULL;
|
||||
int res;
|
||||
const struct spa_dict_item *it;
|
||||
const char *session_name = "unnamed", *service = NULL;
|
||||
uint16_t port = 0;
|
||||
|
||||
if (e->group == NULL) {
|
||||
e->group = avahi_entry_group_new(zc->client,
|
||||
entry_group_callback, e);
|
||||
if (e->group == NULL) {
|
||||
res = avahi_client_errno(zc->client);
|
||||
pw_log_error("can't make group: %s", avahi_strerror(res));
|
||||
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
|
||||
return -EIO;
|
||||
}
|
||||
}
|
||||
avahi_entry_group_reset(e->group);
|
||||
|
||||
spa_dict_for_each(it, &e->props->dict) {
|
||||
if (spa_streq(it->key, "zeroconf.session"))
|
||||
session_name = it->value;
|
||||
else if (spa_streq(it->key, "zeroconf.port"))
|
||||
port = atoi(it->value);
|
||||
else if (spa_streq(it->key, "zeroconf.service"))
|
||||
service = it->value;
|
||||
else
|
||||
txt = avahi_string_list_add_pair(txt, it->key, it->value);
|
||||
}
|
||||
if (service == NULL) {
|
||||
res = -EINVAL;
|
||||
pw_log_error("no service provided");
|
||||
pw_zeroconf_emit_error(zc, res, spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
res = avahi_entry_group_add_service_strlst(e->group,
|
||||
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
||||
(AvahiPublishFlags)0, session_name,
|
||||
service, NULL, NULL, port, txt);
|
||||
avahi_string_list_free(txt);
|
||||
|
||||
if (res < 0) {
|
||||
res = avahi_client_errno(zc->client);
|
||||
pw_log_error("can't add service: %s", avahi_strerror(res));
|
||||
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
|
||||
return -EIO;
|
||||
}
|
||||
if ((res = avahi_entry_group_commit(e->group)) < 0) {
|
||||
res = avahi_client_errno(zc->client);
|
||||
pw_log_error("can't commit group: %s", avahi_strerror(res));
|
||||
pw_zeroconf_emit_error(zc, res, avahi_strerror(res));
|
||||
return -EIO;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int entry_start(struct pw_zeroconf *zc, struct entry *e)
|
||||
{
|
||||
if (zc->state != AVAHI_CLIENT_S_REGISTERING &&
|
||||
zc->state != AVAHI_CLIENT_S_RUNNING &&
|
||||
zc->state != AVAHI_CLIENT_S_COLLISION)
|
||||
return 0;
|
||||
|
||||
if (e->type == TYPE_ANNOUNCE)
|
||||
return do_announce(zc, e);
|
||||
else
|
||||
return do_browse(zc, e);
|
||||
}
|
||||
|
||||
static void client_callback(AvahiClient *c, AvahiClientState state, void *d)
|
||||
{
|
||||
struct pw_zeroconf *zc = d;
|
||||
struct entry *e;
|
||||
|
||||
zc->client = c;
|
||||
zc->refcount++;
|
||||
zc->state = state;
|
||||
|
||||
switch (state) {
|
||||
case AVAHI_CLIENT_S_REGISTERING:
|
||||
case AVAHI_CLIENT_S_RUNNING:
|
||||
case AVAHI_CLIENT_S_COLLISION:
|
||||
spa_list_for_each(e, &zc->entries, link)
|
||||
entry_start(zc, e);
|
||||
break;
|
||||
case AVAHI_CLIENT_FAILURE:
|
||||
{
|
||||
int err = avahi_client_errno(c);
|
||||
pw_zeroconf_emit_error(zc, err, avahi_strerror(err));
|
||||
break;
|
||||
}
|
||||
case AVAHI_CLIENT_CONNECTING:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
zeroconf_unref(zc);
|
||||
}
|
||||
|
||||
static struct entry *entry_new(struct pw_zeroconf *zc, uint32_t type, void *user, const struct spa_dict *info)
|
||||
{
|
||||
struct entry *e;
|
||||
|
||||
if ((e = calloc(1, sizeof(*e))) == NULL)
|
||||
return NULL;
|
||||
|
||||
e->zc = zc;
|
||||
e->type = type;
|
||||
e->user = user;
|
||||
e->props = pw_properties_new_dict(info);
|
||||
spa_list_append(&zc->entries, &e->link);
|
||||
spa_list_init(&e->services);
|
||||
pw_log_info("created %s", type == TYPE_ANNOUNCE ? "announce" : "browse");
|
||||
return e;
|
||||
}
|
||||
|
||||
static int set_entry(struct pw_zeroconf *zc, uint32_t type, void *user, const struct spa_dict *info)
|
||||
{
|
||||
struct entry *e;
|
||||
|
||||
e = entry_find(zc, type, user);
|
||||
if (e == NULL) {
|
||||
if (info == NULL)
|
||||
return 0;
|
||||
e = entry_new(zc, type, user, info);
|
||||
entry_start(zc, e);
|
||||
} else {
|
||||
if (info == NULL)
|
||||
entry_free(e);
|
||||
else {
|
||||
pw_properties_update(e->props, info);
|
||||
entry_start(zc, e);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int pw_zeroconf_set_announce(struct pw_zeroconf *zc, void *user, const struct spa_dict *info)
|
||||
{
|
||||
return set_entry(zc, TYPE_ANNOUNCE, user, info);
|
||||
}
|
||||
int pw_zeroconf_set_browse(struct pw_zeroconf *zc, void *user, const struct spa_dict *info)
|
||||
{
|
||||
return set_entry(zc, TYPE_BROWSE, user, info);
|
||||
}
|
||||
|
||||
struct pw_zeroconf * pw_zeroconf_new(struct pw_context *context,
|
||||
struct spa_dict *props)
|
||||
{
|
||||
struct pw_zeroconf *zc;
|
||||
uint32_t i;
|
||||
int res;
|
||||
|
||||
if ((zc = calloc(1, sizeof(*zc))) == NULL)
|
||||
return NULL;
|
||||
|
||||
zc->refcount = 1;
|
||||
zc->context = context;
|
||||
spa_hook_list_init(&zc->listener_list);
|
||||
spa_list_init(&zc->entries);
|
||||
zc->props = props ? pw_properties_new_dict(props) : pw_properties_new(NULL, NULL);
|
||||
zc->discover_local = true;
|
||||
|
||||
for (i = 0; props && i < props->n_items; i++) {
|
||||
const char *k = props->items[i].key;
|
||||
const char *v = props->items[i].value;
|
||||
|
||||
if (spa_streq(k, "zeroconf.disable-local"))
|
||||
zc->discover_local = spa_atob(v);
|
||||
}
|
||||
|
||||
zc->poll = pw_avahi_poll_new(context);
|
||||
if (zc->poll == NULL)
|
||||
goto error;
|
||||
|
||||
zc->client = avahi_client_new(zc->poll, AVAHI_CLIENT_NO_FAIL,
|
||||
client_callback, zc, &res);
|
||||
if (!zc->client) {
|
||||
pw_log_error("failed to create avahi client: %s", avahi_strerror(res));
|
||||
goto error;
|
||||
}
|
||||
return zc;
|
||||
error:
|
||||
zeroconf_free(zc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void pw_zeroconf_add_listener(struct pw_zeroconf *zc,
|
||||
struct spa_hook *listener,
|
||||
const struct pw_zeroconf_events *events, void *data)
|
||||
{
|
||||
spa_hook_list_append(&zc->listener_list, listener, events, data);
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/* PipeWire */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef PIPEWIRE_ZEROCONF_H
|
||||
#define PIPEWIRE_ZEROCONF_H
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct pw_zeroconf;
|
||||
|
||||
struct pw_zeroconf_events {
|
||||
#define PW_VERSION_ZEROCONF_EVENTS 0
|
||||
uint32_t version;
|
||||
|
||||
void (*destroy) (void *data);
|
||||
void (*error) (void *data, int err, const char *message);
|
||||
|
||||
void (*added) (void *data, void *user, const struct spa_dict *info);
|
||||
void (*removed) (void *data, void *user, const struct spa_dict *info);
|
||||
};
|
||||
|
||||
struct pw_zeroconf * pw_zeroconf_new(struct pw_context *context,
|
||||
struct spa_dict *props);
|
||||
|
||||
void pw_zeroconf_destroy(struct pw_zeroconf *zc);
|
||||
|
||||
int pw_zeroconf_set_announce(struct pw_zeroconf *zc, void *user, const struct spa_dict *info);
|
||||
int pw_zeroconf_set_browse(struct pw_zeroconf *zc, void *user, const struct spa_dict *info);
|
||||
|
||||
void pw_zeroconf_add_listener(struct pw_zeroconf *zc,
|
||||
struct spa_hook *listener,
|
||||
const struct pw_zeroconf_events *events, void *data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PIPEWIRE_ZEROCONF_H */
|
||||
Loading…
Add table
Add a link
Reference in a new issue