mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-03-02 01:40:31 -05:00
sendspin: add sendspin sender and receiver
The sender makes an input stream for each connected client. This makes it easier to do the per client conversion using the adapter and send different channels to clients. The receiver uses linear regression to map ringbuffer indexes to server timestamps and server timestamps to client timestamps. It can then schedule playback against its own clock.
This commit is contained in:
parent
6daa8ccc0d
commit
d6654e84a7
11 changed files with 4644 additions and 2 deletions
|
|
@ -262,7 +262,6 @@ SPA_API_JSON_BUILDER void spa_json_builder_object_uint(struct spa_json_builder *
|
|||
snprintf(str, sizeof(str), "%" PRIu64, val);
|
||||
spa_json_builder_add_simple(b, key, INT_MAX, 'd', str, INT_MAX);
|
||||
}
|
||||
|
||||
SPA_API_JSON_BUILDER void spa_json_builder_object_double(struct spa_json_builder *b,
|
||||
const char *key, double val)
|
||||
{
|
||||
|
|
@ -270,7 +269,6 @@ SPA_API_JSON_BUILDER void spa_json_builder_object_double(struct spa_json_builder
|
|||
spa_json_format_float(str, sizeof(str), (float)val);
|
||||
spa_json_builder_add_simple(b, key, INT_MAX, 'd', str, INT_MAX);
|
||||
}
|
||||
|
||||
SPA_API_JSON_BUILDER void spa_json_builder_object_string(struct spa_json_builder *b,
|
||||
const char *key, const char *val)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -700,6 +700,38 @@ pipewire_module_vban_recv = shared_library('pipewire-module-vban-recv',
|
|||
dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
|
||||
)
|
||||
|
||||
pipewire_module_sendspin_sources = []
|
||||
pipewire_module_sendspin_deps = [ mathlib, dl_lib, rt_lib, pipewire_dep ]
|
||||
|
||||
if avahi_dep.found()
|
||||
pipewire_module_sendspin_sources += [
|
||||
'module-sendspin/zeroconf.c',
|
||||
'module-zeroconf-discover/avahi-poll.c',
|
||||
]
|
||||
pipewire_module_sendspin_deps += avahi_dep
|
||||
endif
|
||||
|
||||
pipewire_module_sendspin_recv = shared_library('pipewire-module-sendspin-recv',
|
||||
[ 'module-sendspin-recv.c',
|
||||
'module-sendspin/websocket.c',
|
||||
pipewire_module_sendspin_sources ],
|
||||
include_directories : [configinc],
|
||||
install : true,
|
||||
install_dir : modules_install_dir,
|
||||
install_rpath: modules_install_dir,
|
||||
dependencies : pipewire_module_sendspin_deps,
|
||||
)
|
||||
pipewire_module_sendspin_send = shared_library('pipewire-module-sendspin-send',
|
||||
[ 'module-sendspin-send.c',
|
||||
'module-sendspin/websocket.c',
|
||||
pipewire_module_sendspin_sources ],
|
||||
include_directories : [configinc],
|
||||
install : true,
|
||||
install_dir : modules_install_dir,
|
||||
install_rpath: modules_install_dir,
|
||||
dependencies : pipewire_module_sendspin_deps,
|
||||
)
|
||||
|
||||
build_module_roc = roc_dep.found()
|
||||
if build_module_roc
|
||||
pipewire_module_roc_sink = shared_library('pipewire-module-roc-sink',
|
||||
|
|
|
|||
1189
src/modules/module-sendspin-recv.c
Normal file
1189
src/modules/module-sendspin-recv.c
Normal file
File diff suppressed because it is too large
Load diff
1389
src/modules/module-sendspin-send.c
Normal file
1389
src/modules/module-sendspin-send.c
Normal file
File diff suppressed because it is too large
Load diff
58
src/modules/module-sendspin/regress.h
Normal file
58
src/modules/module-sendspin/regress.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
struct spa_regress {
|
||||
double meanX;
|
||||
double meanY;
|
||||
double varX;
|
||||
double covXY;
|
||||
uint32_t n;
|
||||
uint32_t m;
|
||||
double a;
|
||||
};
|
||||
|
||||
static inline void spa_regress_init(struct spa_regress *r, uint32_t m)
|
||||
{
|
||||
memset(r, 0, sizeof(*r));
|
||||
r->m = m;
|
||||
r->a = 1.0/m;
|
||||
}
|
||||
static inline void spa_regress_update(struct spa_regress *r, double x, double y)
|
||||
{
|
||||
double a, dx, dy;
|
||||
|
||||
if (r->n == 0) {
|
||||
r->meanX = x;
|
||||
r->meanY = y;
|
||||
r->n++;
|
||||
a = 1.0;
|
||||
} else if (r->n < r->m) {
|
||||
a = 1.0/r->n;
|
||||
r->n++;
|
||||
} else {
|
||||
a = r->a;
|
||||
}
|
||||
dx = x - r->meanX;
|
||||
dy = y - r->meanY;
|
||||
|
||||
r->varX += ((1.0 - a) * dx * dx - r->varX) * a;
|
||||
r->covXY += ((1.0 - a) * dx * dy - r->covXY) * a;
|
||||
r->meanX += dx * a;
|
||||
r->meanY += dy * a;
|
||||
}
|
||||
static inline void spa_regress_get(struct spa_regress *r, double *a, double *b)
|
||||
{
|
||||
*a = r->covXY/r->varX;
|
||||
*b = r->meanY - *a * r->meanX;
|
||||
}
|
||||
static inline double spa_regress_calc_y(struct spa_regress *r, double x)
|
||||
{
|
||||
double a, b;
|
||||
spa_regress_get(r, &a, &b);
|
||||
return x * a + b;
|
||||
}
|
||||
static inline double spa_regress_calc_x(struct spa_regress *r, double y)
|
||||
{
|
||||
double a, b;
|
||||
spa_regress_get(r, &a, &b);
|
||||
return (y - b) / a;
|
||||
}
|
||||
|
||||
27
src/modules/module-sendspin/sendspin.h
Normal file
27
src/modules/module-sendspin/sendspin.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* PipeWire */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef PIPEWIRE_SENDSPIN_H
|
||||
#define PIPEWIRE_SENDSPIN_H
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PW_SENDSPIN_SERVER_SERVICE "_sendspin-server._tcp"
|
||||
#define PW_SENDSPIN_CLIENT_SERVICE "_sendspin._tcp"
|
||||
|
||||
#define PW_SENDSPIN_DEFAULT_SERVER_PORT 8927
|
||||
#define PW_SENDSPIN_DEFAULT_CLIENT_PORT 8928
|
||||
#define PW_SENDSPIN_DEFAULT_PATH "/sendspin"
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PIPEWIRE_SENDSPIN_H */
|
||||
201
src/modules/module-sendspin/teeny-sha1.c
Normal file
201
src/modules/module-sendspin/teeny-sha1.c
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/*******************************************************************************
|
||||
* Teeny SHA-1
|
||||
*
|
||||
* The below sha1digest() calculates a SHA-1 hash value for a
|
||||
* specified data buffer and generates a hex representation of the
|
||||
* result. This implementation is a re-forming of the SHA-1 code at
|
||||
* https://github.com/jinqiangshou/EncryptionLibrary.
|
||||
*
|
||||
* Copyright (c) 2017 CTrabant
|
||||
*
|
||||
* License: MIT, see included LICENSE file for details.
|
||||
*
|
||||
* To use the sha1digest() function either copy it into an existing
|
||||
* project source code file or include this file in a project and put
|
||||
* the declaration (example below) in the sources files where needed.
|
||||
******************************************************************************/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Declaration:
|
||||
extern int sha1digest(uint8_t *digest, char *hexdigest, const uint8_t *data, size_t databytes);
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
* sha1digest: https://github.com/CTrabant/teeny-sha1
|
||||
*
|
||||
* Calculate the SHA-1 value for supplied data buffer and generate a
|
||||
* text representation in hexadecimal.
|
||||
*
|
||||
* Based on https://github.com/jinqiangshou/EncryptionLibrary, credit
|
||||
* goes to @jinqiangshou, all new bugs are mine.
|
||||
*
|
||||
* @input:
|
||||
* data -- data to be hashed
|
||||
* databytes -- bytes in data buffer to be hashed
|
||||
*
|
||||
* @output:
|
||||
* digest -- the result, MUST be at least 20 bytes
|
||||
* hexdigest -- the result in hex, MUST be at least 41 bytes
|
||||
*
|
||||
* At least one of the output buffers must be supplied. The other, if not
|
||||
* desired, may be set to NULL.
|
||||
*
|
||||
* @return: 0 on success and non-zero on error.
|
||||
******************************************************************************/
|
||||
static inline int
|
||||
sha1digest(uint8_t *digest, char *hexdigest, const uint8_t *data, size_t databytes)
|
||||
{
|
||||
#define SHA1ROTATELEFT(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
|
||||
|
||||
uint32_t W[80];
|
||||
uint32_t H[] = {0x67452301,
|
||||
0xEFCDAB89,
|
||||
0x98BADCFE,
|
||||
0x10325476,
|
||||
0xC3D2E1F0};
|
||||
uint32_t a;
|
||||
uint32_t b;
|
||||
uint32_t c;
|
||||
uint32_t d;
|
||||
uint32_t e;
|
||||
uint32_t f = 0;
|
||||
uint32_t k = 0;
|
||||
|
||||
uint32_t idx;
|
||||
uint32_t lidx;
|
||||
uint32_t widx;
|
||||
uint32_t didx = 0;
|
||||
|
||||
int32_t wcount;
|
||||
uint32_t temp;
|
||||
uint64_t databits = ((uint64_t)databytes) * 8;
|
||||
uint32_t loopcount = (databytes + 8) / 64 + 1;
|
||||
uint32_t tailbytes = 64 * loopcount - databytes;
|
||||
uint8_t datatail[128] = {0};
|
||||
|
||||
if (!digest && !hexdigest)
|
||||
return -1;
|
||||
|
||||
if (!data)
|
||||
return -1;
|
||||
|
||||
/* Pre-processing of data tail (includes padding to fill out 512-bit chunk):
|
||||
Add bit '1' to end of message (big-endian)
|
||||
Add 64-bit message length in bits at very end (big-endian) */
|
||||
datatail[0] = 0x80;
|
||||
datatail[tailbytes - 8] = (uint8_t) (databits >> 56 & 0xFF);
|
||||
datatail[tailbytes - 7] = (uint8_t) (databits >> 48 & 0xFF);
|
||||
datatail[tailbytes - 6] = (uint8_t) (databits >> 40 & 0xFF);
|
||||
datatail[tailbytes - 5] = (uint8_t) (databits >> 32 & 0xFF);
|
||||
datatail[tailbytes - 4] = (uint8_t) (databits >> 24 & 0xFF);
|
||||
datatail[tailbytes - 3] = (uint8_t) (databits >> 16 & 0xFF);
|
||||
datatail[tailbytes - 2] = (uint8_t) (databits >> 8 & 0xFF);
|
||||
datatail[tailbytes - 1] = (uint8_t) (databits >> 0 & 0xFF);
|
||||
|
||||
/* Process each 512-bit chunk */
|
||||
for (lidx = 0; lidx < loopcount; lidx++)
|
||||
{
|
||||
/* Compute all elements in W */
|
||||
memset (W, 0, 80 * sizeof (uint32_t));
|
||||
|
||||
/* Break 512-bit chunk into sixteen 32-bit, big endian words */
|
||||
for (widx = 0; widx <= 15; widx++)
|
||||
{
|
||||
wcount = 24;
|
||||
|
||||
/* Copy byte-per byte from specified buffer */
|
||||
while (didx < databytes && wcount >= 0)
|
||||
{
|
||||
W[widx] += (((uint32_t)data[didx]) << wcount);
|
||||
didx++;
|
||||
wcount -= 8;
|
||||
}
|
||||
/* Fill out W with padding as needed */
|
||||
while (wcount >= 0)
|
||||
{
|
||||
W[widx] += (((uint32_t)datatail[didx - databytes]) << wcount);
|
||||
didx++;
|
||||
wcount -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extend the sixteen 32-bit words into eighty 32-bit words, with potential optimization from:
|
||||
"Improving the Performance of the Secure Hash Algorithm (SHA-1)" by Max Locktyukhin */
|
||||
for (widx = 16; widx <= 31; widx++)
|
||||
{
|
||||
W[widx] = SHA1ROTATELEFT ((W[widx - 3] ^ W[widx - 8] ^ W[widx - 14] ^ W[widx - 16]), 1);
|
||||
}
|
||||
for (widx = 32; widx <= 79; widx++)
|
||||
{
|
||||
W[widx] = SHA1ROTATELEFT ((W[widx - 6] ^ W[widx - 16] ^ W[widx - 28] ^ W[widx - 32]), 2);
|
||||
}
|
||||
|
||||
/* Main loop */
|
||||
a = H[0];
|
||||
b = H[1];
|
||||
c = H[2];
|
||||
d = H[3];
|
||||
e = H[4];
|
||||
|
||||
for (idx = 0; idx <= 79; idx++)
|
||||
{
|
||||
if (idx <= 19)
|
||||
{
|
||||
f = (b & c) | ((~b) & d);
|
||||
k = 0x5A827999;
|
||||
}
|
||||
else if (idx >= 20 && idx <= 39)
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
k = 0x6ED9EBA1;
|
||||
}
|
||||
else if (idx >= 40 && idx <= 59)
|
||||
{
|
||||
f = (b & c) | (b & d) | (c & d);
|
||||
k = 0x8F1BBCDC;
|
||||
}
|
||||
else if (idx >= 60 && idx <= 79)
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
k = 0xCA62C1D6;
|
||||
}
|
||||
temp = SHA1ROTATELEFT (a, 5) + f + e + k + W[idx];
|
||||
e = d;
|
||||
d = c;
|
||||
c = SHA1ROTATELEFT (b, 30);
|
||||
b = a;
|
||||
a = temp;
|
||||
}
|
||||
|
||||
H[0] += a;
|
||||
H[1] += b;
|
||||
H[2] += c;
|
||||
H[3] += d;
|
||||
H[4] += e;
|
||||
}
|
||||
|
||||
/* Store binary digest in supplied buffer */
|
||||
if (digest)
|
||||
{
|
||||
for (idx = 0; idx < 5; idx++)
|
||||
{
|
||||
digest[idx * 4 + 0] = (uint8_t) (H[idx] >> 24);
|
||||
digest[idx * 4 + 1] = (uint8_t) (H[idx] >> 16);
|
||||
digest[idx * 4 + 2] = (uint8_t) (H[idx] >> 8);
|
||||
digest[idx * 4 + 3] = (uint8_t) (H[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Store hex version of digest in supplied buffer */
|
||||
if (hexdigest)
|
||||
{
|
||||
snprintf (hexdigest, 41, "%08x%08x%08x%08x%08x",
|
||||
H[0],H[1],H[2],H[3],H[4]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} /* End of sha1digest() */
|
||||
1060
src/modules/module-sendspin/websocket.c
Normal file
1060
src/modules/module-sendspin/websocket.c
Normal file
File diff suppressed because it is too large
Load diff
85
src/modules/module-sendspin/websocket.h
Normal file
85
src/modules/module-sendspin/websocket.h
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* PipeWire */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef PIPEWIRE_WEBSOCKET_H
|
||||
#define PIPEWIRE_WEBSOCKET_H
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct pw_websocket;
|
||||
struct pw_websocket_connection;
|
||||
|
||||
#define PW_WEBSOCKET_OPCODE_TEXT 0x1
|
||||
#define PW_WEBSOCKET_OPCODE_BINARY 0x2
|
||||
#define PW_WEBSOCKET_OPCODE_CLOSE 0x8
|
||||
#define PW_WEBSOCKET_OPCODE_PING 0x9
|
||||
#define PW_WEBSOCKET_OPCODE_PONG 0xa
|
||||
|
||||
struct pw_websocket_connection_events {
|
||||
#define PW_VERSION_WEBSOCKET_CONNECTION_EVENTS 0
|
||||
uint32_t version;
|
||||
|
||||
void (*destroy) (void *data);
|
||||
void (*error) (void *data, int res, const char *reason);
|
||||
void (*disconnected) (void *data);
|
||||
|
||||
void (*message) (void *data,
|
||||
int opcode, void *payload, size_t size);
|
||||
};
|
||||
|
||||
void pw_websocket_connection_add_listener(struct pw_websocket_connection *conn,
|
||||
struct spa_hook *listener,
|
||||
const struct pw_websocket_connection_events *events, void *data);
|
||||
|
||||
void pw_websocket_connection_destroy(struct pw_websocket_connection *conn);
|
||||
void pw_websocket_connection_disconnect(struct pw_websocket_connection *conn, bool drain);
|
||||
|
||||
int pw_websocket_connection_address(struct pw_websocket_connection *conn,
|
||||
struct sockaddr *addr, socklen_t addr_len);
|
||||
|
||||
int pw_websocket_connection_send(struct pw_websocket_connection *conn,
|
||||
uint8_t opcode, const struct iovec *iov, size_t iov_len);
|
||||
|
||||
int pw_websocket_connection_send_text(struct pw_websocket_connection *conn,
|
||||
const char *payload, size_t payload_len);
|
||||
|
||||
|
||||
struct pw_websocket_events {
|
||||
#define PW_VERSION_WEBSOCKET_EVENTS 0
|
||||
uint32_t version;
|
||||
|
||||
void (*destroy) (void *data);
|
||||
|
||||
void (*connected) (void *data, void *user,
|
||||
struct pw_websocket_connection *conn, const char *path);
|
||||
};
|
||||
|
||||
struct pw_websocket * pw_websocket_new(struct pw_loop *main_loop,
|
||||
struct spa_dict *props);
|
||||
|
||||
void pw_websocket_destroy(struct pw_websocket *ws);
|
||||
|
||||
void pw_websocket_add_listener(struct pw_websocket *ws,
|
||||
struct spa_hook *listener,
|
||||
const struct pw_websocket_events *events, void *data);
|
||||
|
||||
int pw_websocket_connect(struct pw_websocket *ws, void *user,
|
||||
const char *hostname, const char *service, const char *path);
|
||||
|
||||
int pw_websocket_listen(struct pw_websocket *ws, void *user,
|
||||
const char *hostname, const char *service, const char *paths);
|
||||
|
||||
int pw_websocket_cancel(struct pw_websocket *ws, void *user);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PIPEWIRE_WEBSOCKET_H */
|
||||
558
src/modules/module-sendspin/zeroconf.c
Normal file
558
src/modules/module-sendspin/zeroconf.c
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
/* 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);
|
||||
}
|
||||
45
src/modules/module-sendspin/zeroconf.h
Normal file
45
src/modules/module-sendspin/zeroconf.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/* 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