/* PipeWire */ /* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */ /* SPDX-License-Identifier: MIT */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "module-sendspin/sendspin.h" #include "module-sendspin/websocket.h" #include "module-sendspin/regress.h" #include "network-utils.h" #ifdef HAVE_AVAHI #include "zeroconf-utils/zeroconf.h" #endif /** \page page_module_sendspin_recv sendspin receiver * * The `sendspin-recv` module creates a PipeWire source that receives audio * packets using the sendspin protocol. * * The receive will listen on a specific port (8928) and create a stream for the * data on the port. * * ## Module Name * * `libpipewire-module-sendspin-recv` * * ## Module Options * * Options specific to the behavior of this module * * - `local.ifname = `: interface name to use * - `source.ip = `: the source ip address to listen on, default 127.0.0.1 * - `source.port = `: the source port to listen on, default 8928 * - `source.path = `: the path to listen on, default "/sendspin" * - `sendspin.ip`: the IP address of the sendspin server * - `sendspin.port`: the port of the sendspin server, default 8927 * - `sendspin.path`: the path on the sendspin server, default "/sendspin" * - `sendspin.client-id`: the client id, default "pipewire-$(hostname)" * - `sendspin.client-name`: the client name, default "$(hostname)" * - `sendspin.autoconnect`: Use zeroconf to connect to an available server, default false. * - `sendspin.announce`: Use zeroconf to announce the client, default true unless * sendspin.autoconnect or sendspin.ip is given. * - `sendspin.single-server`: Allow only a single server to connect, default true * - `node.always-process = `: true to receive even when not running * - `stream.props = {}`: properties to be passed to all the stream * * ## General options * * Options with well-known behavior: * * - \ref PW_KEY_REMOTE_NAME * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_MEDIA_CLASS * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION * - \ref PW_KEY_NODE_GROUP * - \ref PW_KEY_NODE_LATENCY * - \ref PW_KEY_NODE_VIRTUAL * * ## Example configuration *\code{.unparsed} * # ~/.config/pipewire/pipewire.conf.d/my-sendspin-recv.conf * * context.modules = [ * { name = libpipewire-module-sendspin-recv * args = { * #local.ifname = eth0 * #source.ip = 127.0.0.1 * #source.port = 8928 * #source.path = "/sendspin" * #sendspin.ip = 127.0.0.1 * #sendspin.port = 8927 * #sendspin.path = "/sendspin" * #sendspin.client-id = "pipewire-test" * #sendspin.client-name = "PipeWire Test" * #sendspin.autoconnect = false * #sendspin.announce = true * #sendspin.single-server = true * #node.always-process = false * #audio.position = [ FL FR ] * stream.props = { * #media.class = "Audio/Source" * #node.name = "sendspin-receiver" * } * } * } * ] *\endcode * * \since 1.6.0 */ #define NAME "sendspin-recv" PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic #define DEFAULT_SOURCE_IP "127.0.0.1" #define DEFAULT_SOURCE_PORT PW_SENDSPIN_DEFAULT_CLIENT_PORT #define DEFAULT_SOURCE_PATH PW_SENDSPIN_DEFAULT_PATH #define DEFAULT_SERVER_PORT PW_SENDSPIN_DEFAULT_SERVER_PORT #define DEFAULT_SENDSPIN_PATH PW_SENDSPIN_DEFAULT_PATH #define DEFAULT_CREATE_RULES \ "[ { matches = [ { sendspin.ip = \"~.*\" } ] actions = { create-stream = { } } } ] " #define DEFAULT_POSITION "[ FL FR ]" #define USAGE "( local.ifname= ) " \ "( source.ip= ) " \ "( source.port= " \ "( audio.position= ) " \ "( stream.props= { key=value ... } ) " static const struct spa_dict_item module_info[] = { { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, { PW_KEY_MODULE_DESCRIPTION, "sendspin Receiver" }, { PW_KEY_MODULE_USAGE, USAGE }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; struct client { struct impl *impl; struct spa_list link; char *name; struct pw_properties *props; struct pw_websocket_connection *conn; struct spa_hook conn_listener; struct spa_audio_info info; struct pw_stream *stream; struct spa_hook stream_listener; struct pw_timer timer; int timeout_count; uint32_t stride; struct spa_ringbuffer ring; void *buffer; uint32_t buffer_size; #define ROLE_PLAYER (1<<0) #define ROLE_METADATA (1<<1) uint32_t active_roles; #define REASON_DISCOVERY (0) #define REASON_PLAYBACK (1) uint32_t connection_reason; struct spa_regress regress_index; struct spa_regress regress_time; bool resync; struct spa_dll dll; }; struct impl { struct pw_impl_module *module; struct spa_hook module_listener; struct pw_properties *props; struct pw_context *context; struct pw_loop *main_loop; struct pw_loop *data_loop; struct pw_timer_queue *timer_queue; struct pw_core *core; struct spa_hook core_listener; struct spa_hook core_proxy_listener; unsigned int do_disconnect:1; #ifdef HAVE_AVAHI struct pw_zeroconf *zeroconf; struct spa_hook zeroconf_listener; #endif bool always_process; bool single_server; struct pw_properties *stream_props; struct pw_websocket *websocket; struct spa_hook websocket_listener; struct spa_list clients; }; static void on_stream_destroy(void *d) { struct client *client = d; spa_hook_remove(&client->stream_listener); client->stream = NULL; } static void on_stream_state_changed(void *d, enum pw_stream_state old, enum pw_stream_state state, const char *error) { struct client *client = d; switch (state) { case PW_STREAM_STATE_ERROR: case PW_STREAM_STATE_UNCONNECTED: pw_impl_module_schedule_destroy(client->impl->module); break; case PW_STREAM_STATE_PAUSED: case PW_STREAM_STATE_STREAMING: break; default: break; } } static void on_capture_stream_process(void *d) { struct client *client = d; struct pw_buffer *b; struct spa_buffer *buf; uint8_t *p; uint32_t index = 0, n_frames, n_bytes; int32_t avail, stride; struct pw_time ts; double err, corr, target, current_time; if ((b = pw_stream_dequeue_buffer(client->stream)) == NULL) { pw_log_debug("out of buffers: %m"); return; } buf = b->buffer; if ((p = buf->datas[0].data) == NULL) return; stride = client->stride; n_frames = buf->datas[0].maxsize / stride; if (b->requested) n_frames = SPA_MIN(b->requested, n_frames); n_bytes = n_frames * stride; avail = spa_ringbuffer_get_read_index(&client->ring, &index); if (client->timeout_count > 4) { pw_stream_get_time_n(client->stream, &ts, sizeof(ts)); /* index to server time */ target = spa_regress_calc_y(&client->regress_index, index); /* server time to client time */ target = spa_regress_calc_y(&client->regress_time, target); current_time = ts.now / 1000.0; current_time -= (ts.buffered * 1000000.0 / client->info.info.raw.rate) + ((ts.delay) * 1000000.0 * ts.rate.num / ts.rate.denom); err = target - (double)current_time; if (client->resync) { if (target < current_time) { target = spa_regress_calc_x(&client->regress_time, current_time); index = (uint32_t)spa_regress_calc_x(&client->regress_index, target); index = SPA_ROUND_DOWN(index, stride); pw_log_info("resync %u %f %f %f", index, target, current_time, target - current_time); spa_ringbuffer_read_update(&client->ring, index); avail = spa_ringbuffer_get_read_index(&client->ring, &index); err = 0.0; client->resync = false; } else { avail = 0; } } corr = spa_dll_update(&client->dll, SPA_CLAMPD(err, -1000, 1000)); pw_stream_set_rate(client->stream, 1.0 / corr); pw_log_trace("%u %f %f %f %f", index, current_time, target, err, corr); } else { avail = 0; } if (avail < (int32_t)n_bytes) { avail = 0; client->resync = true; } else if (avail > (int32_t)client->buffer_size) { index += avail - client->buffer_size; avail = client->buffer_size; client->resync = true; } if (avail > 0) { n_bytes = SPA_MIN(n_bytes, (uint32_t)avail); spa_ringbuffer_read_data(&client->ring, client->buffer, client->buffer_size, index % client->buffer_size, p, n_bytes); spa_ringbuffer_read_update(&client->ring, index + n_bytes); } else { memset(p, 0, n_bytes); } buf->datas[0].chunk->offset = 0; buf->datas[0].chunk->stride = stride; buf->datas[0].chunk->size = n_bytes; pw_stream_queue_buffer(client->stream, b); } static const struct pw_stream_events capture_stream_events = { PW_VERSION_STREAM_EVENTS, .destroy = on_stream_destroy, .state_changed = on_stream_state_changed, .process = on_capture_stream_process }; static int create_stream(struct client *client) { struct impl *impl = client->impl; int res; uint32_t n_params; const struct spa_pod *params[1]; uint8_t buffer[1024]; struct spa_pod_builder b; const char *server_id, *ip, *port, *server_name; struct pw_properties *props = pw_properties_copy(client->props); ip = pw_properties_get(props, "sendspin.ip"); port = pw_properties_get(props, "sendspin.port"); server_id = pw_properties_get(props, "sendspin.server-id"); server_name = pw_properties_get(props, "sendspin.server-name"); if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) pw_properties_setf(props, PW_KEY_NODE_NAME, "sendspin.%s.%s.%s", server_id, ip, port); if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "Sendspin from %s", server_name); if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL) pw_properties_setf(props, PW_KEY_MEDIA_NAME, "Sendspin from %s", server_name); client->stream = pw_stream_new(impl->core, "sendspin receiver", props); if (client->stream == NULL) return -errno; spa_ringbuffer_init(&client->ring); client->buffer_size = 1024 * 1024; client->buffer = calloc(1, client->buffer_size * client->stride); pw_stream_add_listener(client->stream, &client->stream_listener, &capture_stream_events, client); n_params = 0; spa_pod_builder_init(&b, buffer, sizeof(buffer)); params[n_params++] = spa_format_audio_build(&b, SPA_PARAM_EnumFormat, &client->info); if ((res = pw_stream_connect(client->stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, params, n_params)) < 0) return res; return 0; } static void add_format(struct spa_json_builder *b, const char *codec, int channels, int rate, int depth) { spa_json_builder_array_push(b, "{"); spa_json_builder_object_string(b, "codec", codec); spa_json_builder_object_int(b, "channels", channels); spa_json_builder_object_int(b, "sample_rate", rate); spa_json_builder_object_int(b, "bit_depth", depth); spa_json_builder_pop(b, "}"); } static void add_playerv1_support(struct client *client, struct spa_json_builder *b) { spa_json_builder_object_push(b, "player@v1_support", "{"); spa_json_builder_object_push(b, "supported_formats", "["); add_format(b, "pcm", 2, 48000, 16); add_format(b, "pcm", 1, 48000, 16); spa_json_builder_pop(b, "]"); spa_json_builder_object_int(b, "buffer_capacity", 32000000); spa_json_builder_object_push(b, "supported_commands", "["); spa_json_builder_array_string(b, "volume"); spa_json_builder_array_string(b, "mute"); spa_json_builder_pop(b, "]"); spa_json_builder_pop(b, "}"); } static int send_client_hello(struct client *client) { struct impl *impl = client->impl; struct spa_json_builder b; int res; char *mem; size_t size; spa_json_builder_memstream(&b, &mem, &size, 0); spa_json_builder_array_push(&b, "{"); spa_json_builder_object_string(&b, "type", "client/hello"); spa_json_builder_object_push(&b, "payload", "{"); spa_json_builder_object_string(&b, "client_id", pw_properties_get(impl->props, "sendspin.client-id")); spa_json_builder_object_string(&b, "name", pw_properties_get(impl->props, "sendspin.client-name")); spa_json_builder_object_int(&b, "version", 1); spa_json_builder_object_push(&b, "supported_roles", "["); spa_json_builder_array_string(&b, "player@v1"); spa_json_builder_array_string(&b, "metadata@v1"); spa_json_builder_pop(&b, "]"); spa_json_builder_object_push(&b, "device_info", "{"); spa_json_builder_object_string(&b, "product_name", "Linux"); /* Use os-release */ spa_json_builder_object_stringf(&b, "software_version", "PipeWire %s", pw_get_library_version()); spa_json_builder_pop(&b, "}"); add_playerv1_support(client, &b); spa_json_builder_pop(&b, "}"); spa_json_builder_pop(&b, "}"); spa_json_builder_close(&b); res = pw_websocket_connection_send_text(client->conn, mem, size); free(mem); return res; } static int send_client_state(struct client *client) { struct spa_json_builder b; int res; char *mem; size_t size; spa_json_builder_memstream(&b, &mem, &size, 0); spa_json_builder_array_push(&b, "{"); spa_json_builder_object_string(&b, "type", "client/state"); spa_json_builder_object_push(&b, "payload", "{"); spa_json_builder_object_push(&b, "player", "{"); spa_json_builder_object_string(&b, "state", "synchronized"); spa_json_builder_object_int(&b, "volume", 100); spa_json_builder_object_bool(&b, "muted", false); spa_json_builder_pop(&b, "}"); spa_json_builder_pop(&b, "}"); spa_json_builder_pop(&b, "}"); spa_json_builder_close(&b); res = pw_websocket_connection_send_text(client->conn, mem, size); free(mem); return res; } static uint64_t get_time_us(struct client *client) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return 0; return SPA_TIMESPEC_TO_USEC(&now); } static int send_client_time(struct client *client) { struct spa_json_builder b; int res; uint64_t now; char *mem; size_t size; now = get_time_us(client); spa_json_builder_memstream(&b, &mem, &size, 0); spa_json_builder_array_push(&b, "{"); spa_json_builder_object_string(&b, "type", "client/time"); spa_json_builder_object_push(&b, "payload", "{"); spa_json_builder_object_uint(&b, "client_transmitted", now); spa_json_builder_pop(&b, "}"); spa_json_builder_pop(&b, "}"); spa_json_builder_close(&b); res = pw_websocket_connection_send_text(client->conn, mem, size); free(mem); return res; } static void do_client_timer(void *data) { struct client *client = data; send_client_time(client); } #if 0 static int send_client_command(struct client *client) { return 0; } #endif static int send_client_goodbye(struct client *client, const char *reason) { struct spa_json_builder b; int res; char *mem; size_t size; spa_json_builder_memstream(&b, &mem, &size, 0); spa_json_builder_array_push(&b, "{"); spa_json_builder_object_string(&b, "type", "client/goodbye"); spa_json_builder_object_push(&b, "payload", "{"); spa_json_builder_object_string(&b, "reason", reason); spa_json_builder_pop(&b, "}"); spa_json_builder_pop(&b, "}"); spa_json_builder_close(&b); res = pw_websocket_connection_send_text(client->conn, mem, size); pw_websocket_connection_disconnect(client->conn, true); free(mem); return res; } #if 0 static int send_stream_request_format(struct client *client) { return 0; } #endif static int handle_server_hello(struct client *client, struct spa_json *payload) { struct impl *impl = client->impl; struct spa_json it[1]; char key[256], *t; const char *v; int l, version = 0; struct client *c, *ct; while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { if (spa_streq(key, "server_id")) { t = alloca(l+1); spa_json_parse_stringn(v, l, t, l+1); pw_properties_set(client->props, "sendspin.server-id", t); } else if (spa_streq(key, "name")) { t = alloca(l+1); spa_json_parse_stringn(v, l, t, l+1); pw_properties_set(client->props, "sendspin.server-name", t); } else if (spa_streq(key, "version")) { if (spa_json_parse_int(v, l, &version) <= 0) return -EINVAL; } else if (spa_streq(key, "active_roles")) { if (!spa_json_is_array(v, l)) return -EPROTO; spa_json_enter(payload, &it[0]); while ((l = spa_json_next(&it[0], &v)) > 0) { t = alloca(l+1); spa_json_parse_stringn(v, l, t, l+1); if (spa_streq(t, "player@v1")) client->active_roles |= ROLE_PLAYER; else if (spa_streq(t, "metadata@v1")) client->active_roles |= ROLE_METADATA; } } else if (spa_streq(key, "connection_reason")) { t = alloca(l+1); spa_json_parse_stringn(v, l, t, l+1); if (spa_streq(t, "discovery")) client->connection_reason = REASON_DISCOVERY; else if (spa_streq(t, "playback")) client->connection_reason = REASON_PLAYBACK; pw_properties_set(client->props, "sendspin.connection-reason", t); } } if (version != 1) return -ENOTSUP; if (impl->single_server) { if (client->connection_reason == REASON_PLAYBACK) { /* keep this server, destroy others */ spa_list_for_each_safe(c, ct, &impl->clients, link) { if (c == client) continue; send_client_goodbye(c, "another_server"); } } else { /* keep other servers, destroy this one */ spa_list_for_each_safe(c, ct, &impl->clients, link) { if (c == client) continue; return send_client_goodbye(client, "another_server"); } } } return send_client_state(client); } static int handle_server_state(struct client *client, struct spa_json *payload) { return 0; } static int parse_uint64(const char *val, int len, uint64_t *result) { char buf[64]; char *end; if (len <= 0 || len >= (int)sizeof(buf)) return 0; memcpy(buf, val, len); buf[len] = '\0'; *result = strtoull(buf, &end, 0); return len > 0 && end == buf + len; } static int handle_server_time(struct client *client, struct spa_json *payload) { struct impl *impl = client->impl; char key[256]; const char *v; int l; uint64_t t1 = 0, t2 = 0, t3 = 0, t4 = 0, timeout; t4 = get_time_us(client); while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { if (spa_streq(key, "client_transmitted")) { if (parse_uint64(v, l, &t1) <= 0) return -EINVAL; } else if (spa_streq(key, "server_received")) { if (parse_uint64(v, l, &t2) <= 0) return -EINVAL; } else if (spa_streq(key, "server_transmitted")) { if (parse_uint64(v, l, &t3) <= 0) return -EINVAL; } } spa_regress_update(&client->regress_time, (t2+t3)/2, (t1+t4)/2); if (client->timeout_count < 4) timeout = 200 * SPA_MSEC_PER_SEC; else if (client->timeout_count < 10) timeout = SPA_NSEC_PER_SEC; else if (client->timeout_count < 20) timeout = 2 * SPA_NSEC_PER_SEC; else timeout = 5 * SPA_NSEC_PER_SEC; client->timeout_count++; pw_timer_queue_add(impl->timer_queue, &client->timer, &client->timer.timeout, timeout, do_client_timer, client); return 0; } static int handle_server_command(struct client *client, struct spa_json *payload) { return 0; } /* {"codec":"pcm","sample_rate":44100,"channels":2,"bit_depth":16} */ static int parse_player(struct client *client, struct spa_json *player) { char key[256], codec[64] = ""; const char *v; int l, sample_rate = 0, channels = 0, bit_depth = 0; spa_zero(client->info); client->info.media_type = SPA_MEDIA_TYPE_audio; while ((l = spa_json_object_next(player, key, sizeof(key), &v)) > 0) { if (spa_streq(key, "codec")) { if (spa_json_parse_stringn(v, l, codec, sizeof(codec)) <= 0) return -EINVAL; } else if (spa_streq(key, "sample_rate")) { if (spa_json_parse_int(v, l, &sample_rate) <= 0) return -EINVAL; } else if (spa_streq(key, "channels")) { if (spa_json_parse_int(v, l, &channels) <= 0) return -EINVAL; } else if (spa_streq(key, "bit_depth")) { if (spa_json_parse_int(v, l, &bit_depth) <= 0) return -EINVAL; } else if (spa_streq(key, "codec_header")) { } } if (sample_rate == 0 || channels == 0) return -EINVAL; if (spa_streq(codec, "pcm")) { client->info.media_subtype = SPA_MEDIA_SUBTYPE_raw; client->info.info.raw.rate = sample_rate; client->info.info.raw.channels = channels; switch (bit_depth) { case 16: client->info.info.raw.format = SPA_AUDIO_FORMAT_S16_LE; client->stride = 2 * channels; break; case 24: client->info.info.raw.format = SPA_AUDIO_FORMAT_S24_LE; client->stride = 3 * channels; break; default: return -EINVAL; } } else if (spa_streq(codec, "opus")) { client->info.media_subtype = SPA_MEDIA_SUBTYPE_opus; client->info.info.opus.rate = sample_rate; client->info.info.opus.channels = channels; } else if (spa_streq(codec, "flac")) { client->info.media_subtype = SPA_MEDIA_SUBTYPE_flac; client->info.info.flac.rate = sample_rate; client->info.info.flac.channels = channels; } else return -EINVAL; spa_dll_set_bw(&client->dll, SPA_DLL_BW_MIN, 1000, sample_rate); return 0; } /* {"player":{}} */ static int handle_stream_start(struct client *client, struct spa_json *payload) { struct impl *impl = client->impl; struct spa_json it[1]; char key[256]; const char *v; int l; while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { if (spa_streq(key, "player")) { if (!spa_json_is_object(v, l)) return -EPROTO; spa_json_enter(payload, &it[0]); parse_player(client, &it[0]); } } if (client->stream == NULL) { create_stream(client); pw_timer_queue_cancel(&client->timer); pw_timer_queue_add(impl->timer_queue, &client->timer, NULL, 0, do_client_timer, client); } else { } return 0; } static void stream_clear(struct client *client) { spa_ringbuffer_init(&client->ring); memset(client->buffer, 0, client->buffer_size); } static int handle_stream_clear(struct client *client, struct spa_json *payload) { stream_clear(client); return 0; } static int handle_stream_end(struct client *client, struct spa_json *payload) { if (client->stream != NULL) { pw_stream_destroy(client->stream); client->stream = NULL; stream_clear(client); } return 0; } static int handle_group_update(struct client *client, struct spa_json *payload) { return 0; } /* { "type":... "payload":{...} } */ static int do_parse_text(struct client *client, const char *content, int size) { struct spa_json it[2], *payload = NULL; char key[256], type[256] = ""; const char *v; int res, l; pw_log_info("received text %.*s", size, content); if (spa_json_begin_object(&it[0], content, size) <= 0) return -EINVAL; while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) { if (spa_streq(key, "payload")) { if (!spa_json_is_object(v, l)) return -EPROTO; spa_json_enter(&it[0], &it[1]); payload = &it[1]; } else if (spa_streq(key, "type")) { if (spa_json_parse_stringn(v, l, type, sizeof(type)) <= 0) continue; } } if (spa_streq(type, "server/hello")) res = handle_server_hello(client, payload); else if (spa_streq(type, "server/state")) res = handle_server_state(client, payload); else if (spa_streq(type, "server/time")) res = handle_server_time(client, payload); else if (spa_streq(type, "server/command")) res = handle_server_command(client, payload); else if (spa_streq(type, "stream/start")) res = handle_stream_start(client, payload); else if (spa_streq(type, "stream/end")) res = handle_stream_end(client, payload); else if (spa_streq(type, "stream/clear")) res = handle_stream_clear(client, payload); else if (spa_streq(type, "group/update")) res = handle_group_update(client, payload); else res = 0; return res; } static int do_handle_binary(struct client *client, const uint8_t *payload, int size) { struct impl *impl = client->impl; int32_t filled; uint32_t index, length = size - 9; uint64_t timestamp; if (payload[0] != 4 || client->stream == NULL) return 0; timestamp = ((uint64_t)payload[1]) << 56; timestamp |= ((uint64_t)payload[2]) << 48; timestamp |= ((uint64_t)payload[3]) << 40; timestamp |= ((uint64_t)payload[4]) << 32; timestamp |= ((uint64_t)payload[5]) << 24; timestamp |= ((uint64_t)payload[6]) << 16; timestamp |= ((uint64_t)payload[7]) << 8; timestamp |= ((uint64_t)payload[8]); filled = spa_ringbuffer_get_write_index(&client->ring, &index); if (filled < 0) { pw_log_warn("%p: underrun write:%u filled:%d", client, index, filled); } else if (filled + length > client->buffer_size) { pw_log_debug("%p: overrun write:%u filled:%d", client, index, filled); } spa_ringbuffer_write_data(&client->ring, client->buffer, client->buffer_size, index % client->buffer_size, &payload[9], length); spa_ringbuffer_write_update(&client->ring, index + length); pw_loop_lock(impl->data_loop); spa_regress_update(&client->regress_index, index, timestamp); pw_loop_unlock(impl->data_loop); return 0; } static void on_connection_message(void *data, int opcode, void *payload, size_t size) { struct client *client = data; if (opcode == PW_WEBSOCKET_OPCODE_TEXT) { do_parse_text(client, payload, size); } else if (opcode == PW_WEBSOCKET_OPCODE_BINARY) { do_handle_binary(client, payload, size); } else { pw_log_warn("%02x unknown %08x", opcode, (int)size); } } static void client_free(struct client *client) { struct impl *impl = client->impl; spa_list_remove(&client->link); handle_stream_end(client, NULL); if (client->conn) { spa_hook_remove(&client->conn_listener); pw_websocket_connection_destroy(client->conn); } else { pw_websocket_cancel(impl->websocket, client); } pw_timer_queue_cancel(&client->timer); pw_properties_free(client->props); free(client->buffer); free(client->name); free(client); } static void on_connection_destroy(void *data) { struct client *client = data; client->conn = NULL; pw_log_info("connection %p destroy", client); } static void on_connection_error(void *data, int res, const char *reason) { struct client *client = data; pw_log_error("connection %p error %d %s", client, res, reason); } static void on_connection_disconnected(void *data) { struct client *client = data; client_free(client); } static const struct pw_websocket_connection_events websocket_connection_events = { PW_VERSION_WEBSOCKET_CONNECTION_EVENTS, .destroy = on_connection_destroy, .error = on_connection_error, .disconnected = on_connection_disconnected, .message = on_connection_message, }; static struct client *client_new(struct impl *impl, const char *name, struct pw_properties *props) { struct client *client; client = calloc(1, sizeof(*client)); if (client == NULL) goto error; client->impl = impl; spa_list_append(&impl->clients, &client->link); client->name = name ? strdup(name) : NULL; client->props = props; spa_regress_init(&client->regress_index, 5); spa_regress_init(&client->regress_time, 5); spa_dll_init(&client->dll); client->resync = true; return client; error: pw_properties_free(props); return NULL; } static int client_connect(struct client *c) { struct impl *impl = c->impl; const char *addr, *port, *path; addr = pw_properties_get(c->props, "sendspin.ip"); port = pw_properties_get(c->props, "sendspin.port"); path = pw_properties_get(c->props, "sendspin.path"); return pw_websocket_connect(impl->websocket, c, addr, port, path); } static void client_connected(struct client *c, struct pw_websocket_connection *conn) { if (c->conn) { spa_hook_remove(&c->conn_listener); pw_websocket_connection_destroy(c->conn); } c->conn = conn; if (conn) pw_websocket_connection_add_listener(c->conn, &c->conn_listener, &websocket_connection_events, c); } struct match_info { struct impl *impl; const char *name; struct pw_properties *props; struct pw_websocket_connection *conn; bool matched; }; static int rule_matched(void *data, const char *location, const char *action, const char *str, size_t len) { struct match_info *i = data; struct impl *impl = i->impl; int res = 0; i->matched = true; if (spa_streq(action, "create-stream")) { struct client *c; pw_properties_update_string(i->props, str, len); if ((c = client_new(impl, i->name, spa_steal_ptr(i->props))) == NULL) return -errno; if (i->conn) client_connected(c, i->conn); else client_connect(c); } return res; } static inline int match_client(struct impl *impl, const char *name, struct pw_properties *props, struct pw_websocket_connection *conn) { const char *str; struct match_info minfo = { .impl = impl, .name = name, .props = props, .conn = conn, }; if ((str = pw_properties_get(impl->props, "stream.rules")) == NULL) str = DEFAULT_CREATE_RULES; pw_conf_match_rules(str, strlen(str), NAME, &props->dict, rule_matched, &minfo); if (!minfo.matched) { pw_log_info("unmatched client found %s", str); if (conn) pw_websocket_connection_destroy(conn); pw_properties_free(props); } return minfo.matched; } static void on_websocket_connected(void *data, void *user, struct pw_websocket_connection *conn, const char *path) { struct impl *impl = data; struct client *c = user; pw_log_info("connected to %s", path); if (c == NULL) { struct sockaddr_storage addr; char ip[128]; uint16_t port = 0; bool ipv4; struct pw_properties *props; pw_websocket_connection_address(conn, (struct sockaddr*)&addr, sizeof(addr)); props = pw_properties_copy(impl->stream_props); if (pw_net_get_ip(&addr, ip, sizeof(ip), &ipv4, &port) >= 0) { pw_properties_set(props, "sendspin.ip", ip); pw_properties_setf(props, "sendspin.port", "%u", port); } pw_properties_set(props, "sendspin.path", path); if ((c = client_new(impl, "", props)) == NULL) { pw_log_error("can't create new client: %m"); return; } } client_connected(c, conn); send_client_hello(c); } static const struct pw_websocket_events websocket_events = { PW_VERSION_WEBSOCKET_EVENTS, .connected = on_websocket_connected, }; #ifdef HAVE_AVAHI static struct client *client_find(struct impl *impl, const char *name) { struct client *c; spa_list_for_each(c, &impl->clients, link) { if (spa_streq(c->name, name)) return c; } return NULL; } static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info) { struct impl *impl = data; const char *name, *addr, *port, *path; struct client *c; struct pw_properties *props; name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); if (impl->single_server && !spa_list_is_empty(&impl->clients)) return; if ((c = client_find(impl, name)) != NULL) return; props = pw_properties_copy(impl->stream_props); pw_properties_update(props, info); addr = spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS); port = spa_dict_lookup(info, PW_KEY_ZEROCONF_PORT); path = spa_dict_lookup(info, "path"); pw_properties_set(props, "sendspin.ip", addr); pw_properties_set(props, "sendspin.port", port); pw_properties_set(props, "sendspin.path", path); match_client(impl, name, props, NULL); } static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) { struct impl *impl = data; const char *name; struct client *c; name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); if ((c = client_find(impl, name)) == NULL) return; client_free(c); } static const struct pw_zeroconf_events zeroconf_events = { PW_VERSION_ZEROCONF_EVENTS, .added = on_zeroconf_added, .removed = on_zeroconf_removed, }; #endif static void core_destroy(void *d) { struct impl *impl = d; spa_hook_remove(&impl->core_listener); impl->core = NULL; pw_impl_module_schedule_destroy(impl->module); } static const struct pw_proxy_events core_proxy_events = { .destroy = core_destroy, }; static void impl_destroy(struct impl *impl) { struct client *c; spa_list_consume(c, &impl->clients, link) client_free(c); if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); if (impl->data_loop) pw_context_release_loop(impl->context, impl->data_loop); pw_properties_free(impl->stream_props); pw_properties_free(impl->props); free(impl); } static void module_destroy(void *d) { struct impl *impl = d; spa_hook_remove(&impl->module_listener); impl_destroy(impl); } static const struct pw_impl_module_events module_events = { PW_VERSION_IMPL_MODULE_EVENTS, .destroy = module_destroy, }; static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message) { struct impl *impl = d; pw_log_error("error id:%u seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); if (id == PW_ID_CORE && res == -EPIPE) pw_impl_module_schedule_destroy(impl->module); } static const struct pw_core_events core_events = { PW_VERSION_CORE_EVENTS, .error = on_core_error, }; static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) { const char *str; if ((str = pw_properties_get(props, key)) != NULL) { if (pw_properties_get(impl->stream_props, key) == NULL) pw_properties_set(impl->stream_props, key, str); } } SPA_EXPORT int pipewire__module_init(struct pw_impl_module *module, const char *args) { struct pw_context *context = pw_impl_module_get_context(module); struct impl *impl; const char *str, *hostname, *port, *path; struct pw_properties *props, *stream_props; int res = 0; bool autoconnect; PW_LOG_TOPIC_INIT(mod_topic); impl = calloc(1, sizeof(struct impl)); if (impl == NULL) return -errno; if (args == NULL) args = ""; props = impl->props = pw_properties_new_string(args); stream_props = impl->stream_props = pw_properties_new(NULL, NULL); if (props == NULL || stream_props == NULL) { res = -errno; pw_log_error( "can't create properties: %m"); goto out; } impl->module = module; impl->context = context; impl->main_loop = pw_context_get_main_loop(context); impl->data_loop = pw_context_acquire_loop(context, &props->dict); impl->timer_queue = pw_context_get_timer_queue(context); spa_list_init(&impl->clients); pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name); if ((str = pw_properties_get(props, "stream.props")) != NULL) pw_properties_update_string(stream_props, str, strlen(str)); copy_props(impl, props, PW_KEY_NODE_LOOP_NAME); copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); copy_props(impl, props, PW_KEY_NODE_GROUP); copy_props(impl, props, PW_KEY_NODE_LATENCY); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, PW_KEY_NODE_CHANNELNAMES); copy_props(impl, props, PW_KEY_MEDIA_NAME); copy_props(impl, props, PW_KEY_MEDIA_CLASS); impl->always_process = pw_properties_get_bool(stream_props, PW_KEY_NODE_ALWAYS_PROCESS, true); autoconnect = pw_properties_get_bool(props, "sendspin.autoconnect", false); impl->single_server = pw_properties_get_bool(props, "sendspin.single-server", true); if ((str = pw_properties_get(props, "sendspin.client-name")) == NULL) pw_properties_set(props, "sendspin.client-name", pw_get_host_name()); if ((str = pw_properties_get(props, "sendspin.client-id")) == NULL) pw_properties_setf(props, "sendspin.client-id", "pipewire-%s", pw_get_host_name()); impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); impl->core = pw_context_connect(impl->context, pw_properties_new( PW_KEY_REMOTE_NAME, str, NULL), 0); impl->do_disconnect = true; } if (impl->core == NULL) { res = -errno; pw_log_error("can't connect: %m"); goto out; } pw_proxy_add_listener((struct pw_proxy*)impl->core, &impl->core_proxy_listener, &core_proxy_events, impl); pw_core_add_listener(impl->core, &impl->core_listener, &core_events, impl); impl->websocket = pw_websocket_new(impl->main_loop, &props->dict); pw_websocket_add_listener(impl->websocket, &impl->websocket_listener, &websocket_events, impl); #ifdef HAVE_AVAHI if ((impl->zeroconf = pw_zeroconf_new(context, NULL)) != NULL) { pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, &zeroconf_events, impl); } #endif hostname = pw_properties_get(props, "sendspin.ip"); /* a client should either connect itself or advertize itself and listen * for connections, not both */ if (!autoconnect && hostname == NULL){ /* listen for server connection */ if ((hostname = pw_properties_get(props, "source.ip")) == NULL) hostname = DEFAULT_SOURCE_IP; if ((port = pw_properties_get(props, "source.port")) == NULL) port = SPA_STRINGIFY(DEFAULT_SOURCE_PORT); if ((path = pw_properties_get(props, "source.path")) == NULL) path = DEFAULT_SOURCE_PATH; pw_websocket_listen(impl->websocket, NULL, hostname, port, path); #ifdef HAVE_AVAHI bool announce = pw_properties_get_bool(props, "sendspin.announce", true); if (impl->zeroconf && announce) { /* optionally announce ourselves */ str = pw_properties_get(props, "sendspin.client-id"); pw_zeroconf_set_announce(impl->zeroconf, NULL, &SPA_DICT_ITEMS( SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, PW_SENDSPIN_CLIENT_SERVICE_TYPE), SPA_DICT_ITEM(PW_KEY_ZEROCONF_NAME, str), SPA_DICT_ITEM(PW_KEY_ZEROCONF_PORT, port), SPA_DICT_ITEM("path", path))); } #endif } else { if (hostname != NULL) { struct client *c; struct pw_properties *p; /* connect to hardcoded server */ port = pw_properties_get(props, "sendspin.port"); if (port == NULL) port = SPA_STRINGIFY(DEFAULT_SERVER_PORT); if ((path = pw_properties_get(props, "sendspin.path")) == NULL) path = DEFAULT_SENDSPIN_PATH; p = pw_properties_copy(impl->stream_props); pw_properties_set(p, "sendspin.ip", hostname); pw_properties_set(p, "sendspin.port", port); pw_properties_set(p, "sendspin.path", path); if ((c = client_new(impl, "", p)) != NULL) client_connect(c); } /* connect to zeroconf server if we can */ #ifdef HAVE_AVAHI if (impl->zeroconf) { pw_zeroconf_set_browse(impl->zeroconf, NULL, &SPA_DICT_ITEMS( SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, PW_SENDSPIN_SERVER_SERVICE_TYPE))); } #endif } pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info)); pw_log_info("Successfully loaded module-sendspin-recv"); return 0; out: impl_destroy(impl); return res; }